UI Components Easy
Tip & Split Calculator
Standalone tip calculator with N-way split, tip presets + custom %, pre-tax / post-tax tip basis toggle, and round-up to a friendly number.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: linear-gradient(180deg, var(--cream) 0%, var(--cream-2) 100%);
color: var(--ink);
min-height: 100vh;
display: grid;
place-items: start center;
padding: 32px 16px 48px;
-webkit-font-smoothing: antialiased;
}
.card {
width: 100%;
max-width: 460px;
background: var(--bone);
border-radius: 16px;
padding: 26px 28px 22px;
border: 1px solid rgba(44, 26, 14, 0.08);
box-shadow: 0 12px 36px rgba(44, 26, 14, 0.14);
display: flex;
flex-direction: column;
gap: 16px;
}
.kicker {
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.card h1 {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.7rem;
letter-spacing: -0.015em;
margin-top: 2px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field > span,
fieldset legend {
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-2);
font-weight: 700;
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
fieldset {
border: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.amount-input {
display: inline-flex;
align-items: stretch;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 10px;
overflow: hidden;
}
.amount-input.small input {
width: 70px;
}
.amount-input .sign {
background: var(--cream-2);
padding: 0 12px;
display: grid;
place-items: center;
font-family: var(--font-mono);
font-weight: 700;
color: var(--warm-gray);
}
.amount-input input {
border: none;
background: transparent;
outline: none;
padding: 10px 12px;
font-family: var(--font-mono);
font-weight: 700;
font-size: 1rem;
color: var(--ink);
width: 100%;
}
.amount-input:focus-within {
border-color: var(--terracotta);
}
.basis {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 10px;
padding: 8px 12px;
}
.basis label {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.86rem;
cursor: pointer;
padding: 4px 0;
}
.basis input {
accent-color: var(--forest);
}
.tips {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 10px;
padding: 12px 14px;
}
.tip-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.tip-btn {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 8px 0;
font-family: inherit;
font-size: 0.85rem;
font-weight: 700;
color: var(--ink-2);
cursor: pointer;
}
.tip-btn:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.tip-btn.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.slider {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.slider input {
flex: 1;
accent-color: var(--terracotta);
}
.slider-label {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.95rem;
min-width: 48px;
text-align: right;
}
.slider-label b {
color: var(--terracotta-d);
}
.split {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 10px;
padding: 10px 14px;
}
.party {
display: inline-flex;
align-items: center;
gap: 6px;
}
.party button {
width: 32px;
height: 32px;
border-radius: 999px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.12);
font-family: inherit;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
color: var(--ink);
display: grid;
place-items: center;
}
.party button:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.party button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.party span {
font-family: var(--font-mono);
font-weight: 800;
font-size: 1.2rem;
min-width: 32px;
text-align: center;
}
.roundup {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
border-radius: 10px;
padding: 10px 14px;
}
.round-row {
display: flex;
gap: 4px;
}
.round-btn {
flex: 1;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 7px 8px;
font-family: inherit;
font-size: 0.78rem;
font-weight: 700;
color: var(--ink-2);
cursor: pointer;
}
.round-btn.is-active {
background: var(--gold);
color: var(--ink);
border-color: var(--gold);
}
.totals {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 2px;
}
.totals div {
display: flex;
justify-content: space-between;
font-size: 0.92rem;
color: var(--ink-2);
}
.totals dd {
font-family: var(--font-mono);
font-weight: 700;
}
.totals .big {
margin-top: 6px;
padding-top: 10px;
border-top: 1px solid rgba(44, 26, 14, 0.16);
font-weight: 700;
font-size: 1rem;
color: var(--ink);
}
.totals .big dd {
font-size: 1.15rem;
}
.big-each {
background: var(--forest);
color: var(--bone) !important;
margin: 6px -16px -14px;
padding: 12px 16px;
border-radius: 0 0 12px 12px;
border-top: none !important;
}
.big-each dd {
color: var(--gold) !important;
font-size: 1.35rem !important;
}
.note {
font-size: 0.78rem;
color: var(--warm-gray);
font-style: italic;
min-height: 1.2em;
}const subEl = document.getElementById("subtotal");
const taxEl = document.getElementById("tax");
const sliderEl = document.getElementById("slider");
const sliderVal = document.getElementById("sliderVal");
const partyEl = document.getElementById("party");
const tipBtns = document.querySelectorAll("[data-tip]");
const roundBtns = document.querySelectorAll("[data-round]");
const stepBtns = document.querySelectorAll("[data-step]");
const basisBtns = document.querySelectorAll('input[name="basis"]');
const dSubtotal = document.getElementById("dSubtotal");
const dTax = document.getElementById("dTax");
const dTip = document.getElementById("dTip");
const dTotal = document.getElementById("dTotal");
const dEach = document.getElementById("dEach");
const tTipLabel = document.getElementById("tTipLabel");
const note = document.getElementById("note");
let tip = 18;
let party = 2;
let round = 0;
let basis = "pre";
function money(v) {
return `$${v.toFixed(2)}`;
}
function refresh() {
const sub = Math.max(0, Number(subEl.value) || 0);
const taxRate = Math.max(0, Number(taxEl.value) || 0) / 100;
const tax = sub * taxRate;
const tipBase = basis === "post" ? sub + tax : sub;
const tipAmt = tipBase * (tip / 100);
const total = sub + tax + tipAmt;
dSubtotal.textContent = money(sub);
dTax.textContent = money(tax);
dTip.textContent = money(tipAmt);
dTotal.textContent = money(total);
tTipLabel.textContent = `Tip (${basis === "post" ? "post-tax" : "pre-tax"})`;
let each = total / party;
let extra = 0;
if (round > 0) {
const rounded = Math.ceil(each / round) * round;
extra = (rounded - each) * party;
each = rounded;
}
dEach.textContent = money(each);
note.textContent =
round > 0 && extra > 0
? `Rounded up · the table leaves ${money(extra)} extra ($${(extra).toFixed(2)} total above the bill).`
: tip === 0
? "No tip — server keeps gratuity only if pooled. Consider 15%+ in the US."
: "";
}
tipBtns.forEach((btn) =>
btn.addEventListener("click", () => {
tipBtns.forEach((b) => b.classList.toggle("is-active", b === btn));
tip = Number(btn.dataset.tip);
sliderEl.value = tip;
sliderVal.textContent = tip;
refresh();
})
);
sliderEl.addEventListener("input", (e) => {
tip = Number(e.target.value);
sliderVal.textContent = tip;
tipBtns.forEach((b) => b.classList.toggle("is-active", Number(b.dataset.tip) === tip));
refresh();
});
roundBtns.forEach((btn) =>
btn.addEventListener("click", () => {
roundBtns.forEach((b) => b.classList.toggle("is-active", b === btn));
round = Number(btn.dataset.round);
refresh();
})
);
stepBtns.forEach((btn) =>
btn.addEventListener("click", () => {
party = Math.max(1, Math.min(10, party + Number(btn.dataset.step)));
partyEl.textContent = party;
document.querySelector('[data-step="-1"]').disabled = party <= 1;
document.querySelector('[data-step="1"]').disabled = party >= 10;
refresh();
})
);
basisBtns.forEach((rb) =>
rb.addEventListener("change", () => {
basis = rb.value;
refresh();
})
);
[subEl, taxEl].forEach((el) => el.addEventListener("input", refresh));
refresh();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Tip & Split</title>
</head>
<body>
<section class="card">
<header>
<p class="kicker">Casa Olivar · split helper</p>
<h1>Tip & split</h1>
</header>
<label class="field">
<span>Bill subtotal</span>
<div class="amount-input">
<span class="sign">$</span>
<input id="subtotal" type="number" min="0" step="0.01" value="184.00" />
</div>
</label>
<div class="field-row">
<label class="field">
<span>Tax %</span>
<div class="amount-input small">
<input id="tax" type="number" min="0" max="40" step="0.01" value="8.25" />
<span class="sign sign-right">%</span>
</div>
</label>
<fieldset class="basis">
<legend>Tip on</legend>
<label><input type="radio" name="basis" value="pre" checked /> Pre-tax</label>
<label><input type="radio" name="basis" value="post" /> Post-tax</label>
</fieldset>
</div>
<fieldset class="tips">
<legend>Tip</legend>
<div class="tip-grid">
<button type="button" class="tip-btn" data-tip="15">15%</button>
<button type="button" class="tip-btn is-active" data-tip="18">18%</button>
<button type="button" class="tip-btn" data-tip="20">20%</button>
<button type="button" class="tip-btn" data-tip="22">22%</button>
</div>
<div class="slider">
<input id="slider" type="range" min="0" max="40" step="1" value="18" />
<span class="slider-label"><b id="sliderVal">18</b>%</span>
</div>
</fieldset>
<div class="field-row">
<fieldset class="split">
<legend>Split between</legend>
<div class="party">
<button type="button" data-step="-1" aria-label="Smaller">−</button>
<span id="party">2</span>
<button type="button" data-step="1" aria-label="Larger">+</button>
</div>
</fieldset>
<fieldset class="roundup">
<legend>Round up</legend>
<div class="round-row">
<button type="button" class="round-btn is-active" data-round="0">Off</button>
<button type="button" class="round-btn" data-round="5">$5</button>
<button type="button" class="round-btn" data-round="10">$10</button>
</div>
</fieldset>
</div>
<dl class="totals">
<div><dt>Subtotal</dt><dd id="dSubtotal">$0.00</dd></div>
<div><dt>Tax</dt><dd id="dTax">$0.00</dd></div>
<div><dt id="tTipLabel">Tip (pre-tax)</dt><dd id="dTip">$0.00</dd></div>
<div class="big"><dt>Total</dt><dd id="dTotal">$0.00</dd></div>
<div class="big big-each"><dt>Each pays</dt><dd id="dEach">$0.00</dd></div>
</dl>
<p class="note" id="note"></p>
</section>
<script src="script.js"></script>
</body>
</html>Tip & Split Calculator
The widget servers hand to a guest when they ask “what’s the tip on this?” Editable subtotal, tip preset row (15/18/20/22/custom slider), pre-tax vs post-tax tip basis toggle, N-way split (1–10 with stepper), and a “Round up to nearest” toggle that bumps the split per-person amount to a clean number ($5/$10).