Componentform1.12 KB gzipZero dependencies
Split Bill Calculator
Bill splitter with tip and per-person math.
Preview
Live Preview
Bill total
$
Tip
Ada$44.00
Grace$44.00
Linus$44.00
Total with tip: $132.00
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add split-bill-calculatorOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { SplitBillCalculator } from "@/components/ui/split-bill-calculator";
<SplitBillCalculator defaultTotal={120} />Usage guidance
Use Split Bill Calculator when a ticket stub surface needs Bill splitter with tip and per-person math. Keep props minimal and prefer composition with sibling stubs.
Avoid Split Bill Calculator for long form flows or dense data grids where a dedicated layout block fits better.
Source code
split-bill-calculator.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface SplitBillPerson {
name: string;
paid?: number;
}
export interface SplitBillCalculatorProps extends React.HTMLAttributes<HTMLDivElement> {
total?: number;
defaultTotal?: number;
people?: SplitBillPerson[];
currency?: string;
}
export function SplitBillCalculator({
total: controlledTotal,
defaultTotal = 120,
people = [{ name: "Ada" }, { name: "Grace" }, { name: "Linus" }],
currency = "$",
className,
...props
}: SplitBillCalculatorProps) {
const [innerTotal, setInnerTotal] = React.useState(defaultTotal);
const total = controlledTotal ?? innerTotal;
const [tipPct, setTipPct] = React.useState(10);
const withTip = total * (1 + tipPct / 100);
const perPerson = people.length > 0 ? withTip / people.length : 0;
return (
<div className={cn("w-full max-w-sm rounded-lg border border-border bg-card p-4", className)} {...props}>
<div className="flex items-center justify-between">
<span className="font-mono text-[11px] uppercase tracking-widest text-muted-foreground">Bill total</span>
<div className="flex items-center gap-1 font-mono text-sm">
<span className="text-muted-foreground">{currency}</span>
<input
value={total}
inputMode="decimal"
aria-label="Bill total"
onChange={(e) => setInnerTotal(Number(e.target.value.replace(/[^0-9.]/g, "")) || 0)}
className="w-20 bg-transparent text-right font-bold tabular-nums focus:outline-none"
/>
</div>
</div>
<div className="mt-3 flex items-center gap-2">
<span className="font-mono text-[11px] uppercase text-muted-foreground">Tip</span>
{[0, 10, 15, 20].map((t) => (
<button
key={t}
type="button"
aria-pressed={tipPct === t}
onClick={() => setTipPct(t)}
className={cn(
"cursor-pointer rounded border px-2 py-1 font-mono text-[11px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
tipPct === t ? "border-foreground bg-primary font-bold text-primary-foreground" : "border-border text-muted-foreground hover:text-foreground",
)}
>
{t}%
</button>
))}
</div>
<div className="mt-3 space-y-1.5 border-t border-dashed border-border pt-3">
{people.map((p) => (
<div key={p.name} className="flex items-center justify-between font-mono text-sm">
<span>{p.name}</span>
<strong className="tabular-nums">
{currency}
{perPerson.toFixed(2)}
</strong>
</div>
))}
</div>
<p className="mt-3 border-t border-dashed border-border pt-2 text-right font-mono text-xs text-muted-foreground">
Total with tip:{" "}
<strong className="text-base text-foreground tabular-nums">
{currency}
{withTip.toFixed(2)}
</strong>
</p>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| total | number | Controlled bill total. |
| people | SplitBillPerson[] | Diners list. |
| currency | string | Currency symbol (default '$'). |