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-liveresult 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);
})();
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.