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

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    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






    lang="en">

    charset="UTF-8">
    Tip Calculator
    rel="stylesheet" href="style.css">



    class="card">
    Tip Calculator

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

    Tip percentage
    class="tip-buttons">
    class="tip-btn" data-tip="15">15%
    class="tip-btn" data-tip="18">18%
    class="tip-btn" data-tip="20">20%
    class="tip-btn" data-tip="25">25%


    type="number" id="custom-tip" min="0" max="100" placeholder="Custom %">

    Number of people
    type="number" id="people" min="1" value="1">

    class="results">
    class="result-row">
    Tip amount
    id="tip-amount">$0.00


    class="result-row">
    Total
    id="total">$0.00


    class="result-row highlight">
    Per person
    id="per-person">$0.00







    "app.js">












    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.




    More...
Working...