Project: a tip calculator
In this lesson
- What you will build
- How to work: build in small steps
- Step 1: the form
- Step 2: places for the answers
- Step 3: make it look good
- Step 4: work out the tip
Practise this in the Code Studio
open the guided build, then build your own calculator. You type real code in your browser, see the result at once, and the tasks tick themselves when your code is right. Nothing to install.
Your first project is small enough to finish in one sitting and real enough to use at a restaurant tonight. You enter the bill, choose a tip and the number of people, and the page works out the tip, the total and what each person pays.
It uses the three languages together: HTML for the form, CSS to make it look good, and JavaScript to do the thinking. If you have worked through the HTML, CSS and JavaScript courses, you already know every piece. This lesson shows you how to put them together.
What you will build
- A form with a bill, a tip drop-down and a number of people.
- A Calculate button that shows the tip, the total and each person's share.
- Friendly messages when the input does not make sense, instead of broken answers.
How to work: build in small steps
Beginners often try to write everything at once and then cannot find what broke. Professionals do the opposite: build one small thing, check that it works, then add the next. The Code Studio follows that rhythm: each step is a task that ticks when it works.
1The form
Every input gets an id, so JavaScript can find it, and every input gets a <label>, so people (and screen readers) know what it is for. Clicking a label also puts the cursor in its box.
<label for="bill">Bill (R)</label>
<input id="bill" type="number" min="0" step="0.01" placeholder="250">
<label for="tip">Tip</label>
<select id="tip">
<option value="10">10%</option>
<option value="15" selected>15%</option>
<option value="20">20%</option>
</select>type="number" gives phones a number keypad. step="0.01" allows cents. The value of each option is the number your code will use; the text between the tags is what people read.
2Places for the answers
Plan where every answer will appear before you write any JavaScript. An empty element with an id is a place waiting for text.
<p id="error" role="alert"></p>
<dl id="result" hidden>
<dt>Tip</dt><dd id="tipAmount"></dd>
<dt>Total</dt><dd id="total"></dd>
<dt>Each person pays</dt><dd id="each"></dd>
</dl>A <dl> is a description list: terms (dt) and their descriptions (dd). The hidden attribute keeps the results out of sight until there is something to show.
3Make it look good
A calculator that looks cared for feels trustworthy. A card (a white box on a soft background) is the simplest good-looking layout there is.
body {
background: #f3f6f9;
font-family: Verdana, sans-serif;
padding: 24px;
}
.app {
background: #fff;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
margin: 0 auto;
max-width: 420px;
padding: 24px;
}
input, select {
box-sizing: border-box;
width: 100%;
}margin: 0 auto centres a box that has a width. max-width lets it shrink on a phone. box-sizing: border-box makes width: 100% include the padding, so the boxes do not poke out of the card.
4Work out the tip
Now the JavaScript. Find the elements once at the top, then listen for the button.
const bill = document.querySelector("#bill");
const tip = document.querySelector("#tip");
const people = document.querySelector("#people");
document.querySelector("#calc").addEventListener("click", () => {
const billAmount = Number(bill.value);
const tipAmount = billAmount * Number(tip.value) / 100;
document.querySelector("#tipAmount").textContent = "R " + tipAmount.toFixed(2);
});Remember
a box's
.valueis always text, even in a number box.Number("200")turns it into the number 200. Without it,"200" + 30would give"20030".
toFixed(2) rounds to two decimal places and returns text such as "30.00", which is exactly what money should look like.
5The total and each person's share
const total = billAmount + tipAmount;
document.querySelector("#total").textContent = "R " + total.toFixed(2);
document.querySelector("#each").textContent = "R " + (total / count).toFixed(2);6Protect it against bad input
People will click Calculate with an empty box, or type a negative bill. Real programs expect that. Check first, then show a message and stop with return.
error.textContent = "";
if (!(billAmount > 0)) {
error.textContent = "Enter the bill amount first.";
result.hidden = true;
return;
}Tip
write the test as
!(billAmount > 0)instead ofbillAmount <= 0. An empty box givesNumber(""), which is0, but some bad input givesNaN(not a number).NaN > 0isfalse, so!(NaN > 0)istrue: one test catches every bad case.
7Polish
Zero people would divide by zero and show Infinity. Force the count to be at least one:
const count = Math.max(1, Math.floor(Number(people.value)) || 1);Read it from the inside out. Number(people.value) is the number typed. Math.floor removes any decimals. || 1 replaces 0 or NaN (both false-like) with 1. Math.max(1, …) makes sure the result is never below 1.
The finished project
<main class="app">
<h1>Tip calculator</h1>
<label for="bill">Bill (R)</label>
<input id="bill" type="number" min="0" step="0.01" placeholder="250">
<label for="tip">Tip</label>
<select id="tip">
<option value="10">10%</option>
<option value="15" selected>15%</option>
<option value="20">20%</option>
</select>
<label for="people">People</label>
<input id="people" type="number" min="1" value="1">
<button id="calc" type="button">Calculate</button>
<p id="error" role="alert"></p>
<dl id="result" hidden>
<dt>Tip</dt><dd id="tipAmount"></dd>
<dt>Total</dt><dd id="total"></dd>
<dt>Each person pays</dt><dd id="each"></dd>
</dl>
</main>body {
background: #f3f6f9;
color: #1a2940;
font-family: Verdana, sans-serif;
margin: 0;
padding: 24px;
}
.app {
background: #fff;
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
margin: 0 auto;
max-width: 420px;
padding: 24px;
}
label {
display: block;
font-weight: 700;
margin: 14px 0 4px;
}
input,
select {
border: 1px solid #c9d4df;
border-radius: 8px;
box-sizing: border-box;
font: inherit;
padding: 10px;
width: 100%;
}
#calc {
background: #1f74bf;
border: 0;
border-radius: 8px;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 700;
margin-top: 18px;
padding: 12px;
width: 100%;
}
#calc:hover {
background: #155a98;
}
#error {
color: #c0392b;
min-height: 1.2em;
}
dt {
color: #5d7088;
margin-top: 10px;
}
dd {
font-size: 1.4rem;
font-weight: 700;
margin: 0;
}const bill = document.querySelector("#bill");
const tip = document.querySelector("#tip");
const people = document.querySelector("#people");
const error = document.querySelector("#error");
const result = document.querySelector("#result");
function money(amount) {
return "R " + amount.toFixed(2);
}
document.querySelector("#calc").addEventListener("click", () => {
const billAmount = Number(bill.value);
const count = Math.max(1, Math.floor(Number(people.value)) || 1);
error.textContent = "";
if (!(billAmount > 0)) {
error.textContent = "Enter the bill amount first.";
result.hidden = true;
return;
}
const tipAmount = billAmount * Number(tip.value) / 100;
const total = billAmount + tipAmount;
document.querySelector("#tipAmount").textContent = money(tipAmount);
document.querySelector("#total").textContent = money(total);
document.querySelector("#each").textContent = money(total / count);
result.hidden = false;
});Make it yours
Once all seven tasks tick, change it until it feels like yours:
- Your currency and your colours. Change
Rto your currency and replace the blues with a palette you like. - A custom tip. Add a 4th option, "Other", that shows a box for a percentage.
- Round up. Add a checkbox "Round each share up to the nearest rand" and use
Math.ceil. - Calculate as you type. Listen to the
inputevent on the boxes as well, so there is no need for the button.
Your turn: build a calculator of your own
Marked like an exam
this project shows no hints and no answers while you work, and nothing ticks until you submit. Then you get your mark, the feedback and the model answers, and you can try again.
Open the second exercise. There are no steps this time, only a list of what your calculator must do. You choose what it calculates.
Pick one:
- A BMI calculator (weight ÷ height²).
- A unit converter (kilometres to miles, °C to °F).
- A discount calculator (price and percentage off).
- An age in days calculator.
- A grade average calculator.
- Anything you wish existed.
It must have labelled inputs, react to the button, give different answers for different inputs, survive empty boxes, use a function of your own and look good. When the tasks tick, press Download page to keep a copy.
Check yourself
Why do we write Number(bill.value)?
The value of an input is always text. Number() converts the text to a number so we can do arithmetic. Without it, + would join pieces of text together.
What does toFixed(2) return, and why is it useful for money?
It rounds a number to two decimal places and returns it as text, for example "15.00". Money is shown in cents, and 15 or 14.9985 would look wrong.
Why does the code use return after showing an error message?
return ends the function. Without it the code would carry on and show a wrong answer under the error message.
Why give every input an id and a label?
The id lets JavaScript find the element, and the for attribute of the label connects the text to it. People can click the label to focus the box, and screen readers read the label aloud.


