Tip Calculator with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #06

Project Overview

A friendly Tip Calculator built with HTML, CSS and JavaScript. The app calculates the tip amount and total per person based on the bill amount, selected tip percentage (quick buttons or custom), and number of people splitting the bill. It demonstrates handling form inputs, simple math logic, input validation, and live UI updates.

Key Features

  • Enter bill amount and number of people.
  • Quick-select tip percentage buttons (5%, 10%, 15%, 25%, 50%) plus a custom input.
  • Calculates tip per person and total per person in real-time.
  • Handles edge cases (0 people or invalid input) with user-friendly error feedback.
  • Reset button to clear inputs and results.
  • Responsive, accessible layout with aria-live result updates.

HTML Code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 6 — Tip Calculator</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div class="logo">TC</div>
      <div>
        <h1 id="title">Day 6: Tip Calculator</h1>
        <p class="lead">Calculate tip per person and total per person quickly. Built with simple inputs and math logic.</p>
      </div>
    </header>

    <section class="calc-grid">
      <form id="tipForm" class="controls" novalidate>
        <label>
          Bill amount
          <input id="bill" type="number" min="0" step="0.01" placeholder="0.00" inputmode="decimal" required>
        </label>

        <fieldset class="tip-buttons" aria-label="Tip percentage">
          <legend>Select Tip %</legend>
          <div class="buttons">
            <button type="button" class="tipBtn" data-tip="5">5%</button>
            <button type="button" class="tipBtn" data-tip="10">10%</button>
            <button type="button" class="tipBtn" data-tip="15">15%</button>
            <button type="button" class="tipBtn" data-tip="25">25%</button>
            <button type="button" class="tipBtn" data-tip="50">50%</button>
            <input id="customTip" type="number" min="0" step="1" placeholder="Custom" inputmode="numeric">
          </div>
        </fieldset>

        <label>
          Number of people
          <input id="people" type="number" min="1" step="1" placeholder="1" inputmode="numeric" required>
        </label>

        <div class="form-actions">
          <button id="resetBtn" type="button" class="btn secondary">Reset</button>
        </div>
      </form>

      <aside class="results" aria-live="polite">
        <div class="result-row">
          <div class="label">Tip amount <span class="muted">/ person</span></div>
          <div id="tipPerPerson" class="value">₹0.00</div>
        </div>
        <div class="result-row">
          <div class="label">Total <span class="muted">/ person</span></div>
          <div id="totalPerPerson" class="value">₹0.00</div>
        </div>

        <div id="error" class="error" aria-live="assertive"></div>
      </aside>
    </section>

    <details style="margin-top:12px">
      <summary>How it works (short)</summary>
      <p class="small">JS reads bill, tip%, and people inputs, computes tip = bill * (tip%/100), tip per person = tip / people, total per person = (bill/people) + tip per person. Validation prevents division by zero and invalid input.</p>
    </details>
  </main>

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

CSS Code

:root {
  --bg: #0f1724;
  --card: #071027;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  --danger: #ef4444;
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: #002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(820px, 96%);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 8px 30px rgba(2, 6, 23, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 14px;
  align-items: center;
}

.logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--accent), #22c1c3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px;
}

.calc-grid {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 18px;
  align-items: start;
}

@media (max-width:800px) {
  .calc-grid {
    grid-template-columns: 1fr;
  }
}

.controls {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.controls label {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 14px;
  color: var(--muted);
}

.controls input[type="number"] {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
  outline: none;
}

.controls input:focus {
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.08);
  border-color: rgba(99, 102, 241, 0.6);
}

.tip-buttons .buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

.tipBtn {
  padding: 10px 12px;
  border-radius: 8px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer
}

.tipBtn.inactive {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted)
}

.tip-buttons input {
  padding: 8px 10px;
  min-width: 80px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
}

.form-actions {
  display: flex;
  gap: 8px;
  margin-top: 6px;
}

.btn {
  padding: 10px 14px;
  border-radius: 10px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer
}

.btn.secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted)
}

.results {
  background: rgba(255, 255, 255, 0.02);
  padding: 16px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.03);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.result-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.label {
  color: var(--muted);
  font-size: 14px;
}

.value {
  font-family: ui-monospace, monospace;
  font-size: 20px;
  font-weight: 700;
}

.muted {
  font-size: 12px;
  color: var(--muted)
}

.error {
  color: var(--danger);
  font-size: 13px;
  min-height: 18px
}

.small {
  font-size: 13px;
  color: var(--muted)
}

Javascript Code

// Tip Calculator logic

// Elements
const billInput = document.getElementById('bill');
const peopleInput = document.getElementById('people');
const tipButtons = Array.from(document.querySelectorAll('.tipBtn'));
const customTipInput = document.getElementById('customTip');
const tipPerPersonEl = document.getElementById('tipPerPerson');
const totalPerPersonEl = document.getElementById('totalPerPerson');
const errorEl = document.getElementById('error');
const resetBtn = document.getElementById('resetBtn');

let selectedTip = null; // percentage as number (e.g., 15 for 15%)

// Helpers
function formatCurrency(value) {
  // Use simple rupee symbol and 2 decimals. Replace with Intl if desired.
  return '₹' + Number(value).toFixed(2);
}

function clearTipSelection() {
  selectedTip = null;
  tipButtons.forEach(b => b.classList.remove('inactive'));
  customTipInput.value = '';
}

// Update calculations
function calculate() {
  errorEl.textContent = '';
  const bill = parseFloat(billInput.value);
  const people = parseInt(peopleInput.value, 10);

  // Determine tip percentage
  let tipPct = selectedTip;
  const customVal = parseFloat(customTipInput.value);
  if (!isNaN(customVal) && customVal >= 0) tipPct = customVal;

  // Validate inputs
  if (isNaN(bill) || bill < 0) {
    tipPerPersonEl.textContent = formatCurrency(0);
    totalPerPersonEl.textContent = formatCurrency(0);
    errorEl.textContent = 'Enter a valid bill amount.';
    return;
  }
  if (isNaN(people) || people <= 0) {
    tipPerPersonEl.textContent = formatCurrency(0);
    totalPerPersonEl.textContent = formatCurrency(0);
    errorEl.textContent = 'Number of people must be at least 1.';
    return;
  }
  if (tipPct === null || isNaN(tipPct) || tipPct < 0) {
    // treat no tip as 0%
    tipPct = 0;
  }

  // Math:
  // total tip = bill * (tipPct/100)
  // tip per person = total tip / people
  // total per person = (bill / people) + tip per person
  const totalTip = bill * (tipPct / 100);
  const tipPerPerson = totalTip / people;
  const totalPerPerson = (bill / people) + tipPerPerson;

  tipPerPersonEl.textContent = formatCurrency(tipPerPerson);
  totalPerPersonEl.textContent = formatCurrency(totalPerPerson);
  errorEl.textContent = '';
}

// Tip button clicks
tipButtons.forEach(btn => {
  btn.addEventListener('click', (e) => {
    // toggle button as selected/inactive
    const pct = Number(btn.dataset.tip);
    if (selectedTip === pct) {
      // deselect
      selectedTip = null;
      btn.classList.remove('inactive');
    } else {
      selectedTip = pct;
      tipButtons.forEach(b => b.classList.remove('inactive'));
      btn.classList.add('inactive'); // visually mark selected (uses same class for styling)
      // clear custom input
      customTipInput.value = '';
    }
    calculate();
  });
});

// custom tip input
customTipInput.addEventListener('input', () => {
  // clear selected quick buttons when user types custom
  selectedTip = null;
  tipButtons.forEach(b => b.classList.remove('inactive'));
  calculate();
});

// inputs update
[billInput, peopleInput].forEach(inp => inp.addEventListener('input', calculate));

// Reset
resetBtn.addEventListener('click', () => {
  billInput.value = '';
  peopleInput.value = '';
  customTipInput.value = '';
  clearTipSelection();
  tipPerPersonEl.textContent = formatCurrency(0);
  totalPerPersonEl.textContent = formatCurrency(0);
  errorEl.textContent = '';
});

// initialize defaults
(function init(){
  billInput.value = '';
  peopleInput.value = '1';
  clearTipSelection();
  tipPerPersonEl.textContent = formatCurrency(0);
  totalPerPersonEl.textContent = formatCurrency(0);
})();
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 4 : Stop-Watch App

A simple stopwatch with start, stop, and reset buttons.

Concepts: setInterval(), clearInterval(), time logic.

Day 8 : Movie Search App

Search movies and display results with posters using an API.

Concepts: API integration, async/await.

Day 9 : Flashcard Learning App

Create and flip flashcards to study and test your knowledge.

Concepts: DOM manipulation, CSS 3D effects, LocalStorage.