Technology Apr 30, 2026 · 5 min read

Build a Tip Calculator in Vanilla JavaScript: a beginner DOM project

A tip calculator is a great first DOM project. It requires user input, real-time calculation, and clean output — but no API calls, no data storage, no complexity you don't control. By the end of this post you'll have a working tip calculator that handles bill splitting too. The math B...

DE
DEV Community
by Snappy Tools
Build a Tip Calculator in Vanilla JavaScript: a beginner DOM project

A tip calculator is a great first DOM project. It requires user input, real-time calculation, and clean output — but no API calls, no data storage, no complexity you don't control. By the end of this post you'll have a working tip calculator that handles bill splitting too.

The math

Before writing any code, get the arithmetic right:

tip amount   = bill × (tip% / 100)
total        = bill + tip amount
per person   = total / number of people

That's it. The calculator is just this formula wired to input fields.

Step 1 — HTML structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tip Calculator</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

<div class="card">
  <h1>Tip Calculator</h1>

  <label>Bill amount ($)</label>
  <input type="number" id="bill" min="0" step="0.01" placeholder="0.00">

  <label>Tip percentage</label>
  <div class="tip-buttons">
    <button class="tip-btn" data-tip="15">15%</button>
    <button class="tip-btn" data-tip="18">18%</button>
    <button class="tip-btn" data-tip="20">20%</button>
    <button class="tip-btn" data-tip="25">25%</button>
  </div>
  <input type="number" id="custom-tip" min="0" max="100" placeholder="Custom %">

  <label>Number of people</label>
  <input type="number" id="people" min="1" value="1">

  <div class="results">
    <div class="result-row">
      <span>Tip amount</span>
      <span id="tip-amount">$0.00</span>
    </div>
    <div class="result-row">
      <span>Total</span>
      <span id="total">$0.00</span>
    </div>
    <div class="result-row highlight">
      <span>Per person</span>
      <span id="per-person">$0.00</span>
    </div>
  </div>
</div>

<script src="app.js"></script>
</body>
</html>

Step 2 — JavaScript

The key insight: recalculate on every input event. Don't make the user press a button.

// app.js

let selectedTip = 18; // default

// Tip percentage buttons
document.querySelectorAll('.tip-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    // Deselect all, select this one
    document.querySelectorAll('.tip-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');

    selectedTip = parseFloat(btn.dataset.tip);

    // Clear custom input when a preset is selected
    document.getElementById('custom-tip').value = '';

    calculate();
  });
});

// Custom tip input
document.getElementById('custom-tip').addEventListener('input', () => {
  // Deselect preset buttons
  document.querySelectorAll('.tip-btn').forEach(b => b.classList.remove('active'));

  const val = parseFloat(document.getElementById('custom-tip').value);
  selectedTip = isNaN(val) ? 0 : val;
  calculate();
});

// Bill and people inputs
document.getElementById('bill').addEventListener('input', calculate);
document.getElementById('people').addEventListener('input', calculate);

function calculate() {
  const bill   = parseFloat(document.getElementById('bill').value)   || 0;
  const people = parseInt(document.getElementById('people').value)   || 1;
  const tip    = selectedTip;

  const tipAmount = bill * (tip / 100);
  const total     = bill + tipAmount;
  const perPerson = total / people;

  // Format as currency
  document.getElementById('tip-amount').textContent = '$' + tipAmount.toFixed(2);
  document.getElementById('total').textContent      = '$' + total.toFixed(2);
  document.getElementById('per-person').textContent = '$' + perPerson.toFixed(2);
}

// Run once on load to set initial state
calculate();

Step 3 — Minimal CSS

/* style.css */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #f5f6fa;
  margin: 0;
}

.card {
  background: white;
  border-radius: 16px;
  padding: 32px;
  width: 360px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}

label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: #555;
  margin: 16px 0 6px;
}

input[type="number"] {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 1rem;
  box-sizing: border-box;
}

.tip-buttons {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.tip-btn {
  flex: 1;
  padding: 8px;
  border: 2px solid #ddd;
  border-radius: 8px;
  background: white;
  font-weight: 600;
  cursor: pointer;
}

.tip-btn.active {
  border-color: #2f855a;
  background: #f0fff4;
  color: #2f855a;
}

.results {
  margin-top: 24px;
  border-top: 1px solid #eee;
  padding-top: 16px;
}

.result-row {
  display: flex;
  justify-content: space-between;
  padding: 8px 0;
}

.result-row.highlight {
  font-size: 1.25rem;
  font-weight: 700;
  color: #2f855a;
}

Edge cases to handle

The simple version above works, but a production-ready calculator handles a few more situations:

Negative bills and zero people

function calculate() {
  const bill   = Math.max(0, parseFloat(document.getElementById('bill').value) || 0);
  const people = Math.max(1, parseInt(document.getElementById('people').value) || 1);
  // ...
}

Math.max(0, ...) prevents negative bills. Math.max(1, ...) prevents dividing by zero.

Rounding errors

0.1 + 0.2 === 0.30000000000000004 in JavaScript. Always display with .toFixed(2) and only do the rounding at display time, not during calculation.

Non-numeric input

parseFloat('abc') returns NaN. The || 0 fallback in const bill = parseFloat(...) || 0 handles this cleanly.

What makes a tip calculator feel polished

A few details that separate a demo from something people actually use:

  1. Default to 18–20% so the output is meaningful immediately
  2. Update on every keystroke — no "calculate" button
  3. Highlight the per-person amount — that's the number people care about at the table
  4. Handle 1 person cleanly — don't show "per person" math if splitting with 1 person

Try a working version

If you want to see a complete implementation — including tip pooling, dark mode, and mobile layout — SnappyTools Tip Calculator is a free, no-signup version you can use as a reference.

What to build next

Once this is working, natural extensions:

  • Rounding mode: round each person's share up to the nearest dollar
  • Tip pool distribution: split tips among multiple staff at different percentage weights
  • Currency formatting: Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(n)
  • Persistent state: localStorage to remember the last-used tip percentage

The Intl.NumberFormat approach is cleaner than manual string building once you need real currency formatting.

DE
Source

This article was originally published by DEV Community and written by Snappy Tools.

Read original article on DEV Community
Back to Discover

Reading List