Call it even.
A no-account, browser-only expense splitter that finds the shortest path to settled.
Three split modes, minimum-transactions settlement, and shareable receipts in seconds.
What it does · Split modes · Tech stack · Getting started · Privacy · Design · Roadmap
Splitting a group bill takes more time than the meal that caused it. Splitwise is overkill for a one-off court hire or a single dinner. Notes-app math gets messy the moment people pre-paid different amounts. And when the booking was time-based, padel, tennis, an hourly studio rental, equal splits feel unfair if one player only showed up for the second half.
A pure-frontend expense splitter with three modes, a minimum-transactions settlement algorithm, and a participation-aware time-based split for shared activity bookings. No account, no server, no data leaving your browser unless you explicitly share. Live updates as you type, motion-rich UI, and a shareable receipt that exports as text, image, or URL.
| Mode | What it does | Best for |
|---|---|---|
| Equal | Total divided by N people | Dinners, group cabs, shared groceries |
| Custom % | Each person sets a percentage; live progress bar enforces 100% | Uneven contributions, weighted shares |
| By Time | Each person pays in proportion to minutes used vs the booking duration | Padel courts, tennis sessions, hourly rentals |
Greedy debtor-to-creditor matching: pairs the largest debt with the largest credit, settles the overlap, repeats. The result is the smallest possible number of transfers to leave the group square.
In By Time mode, each person gets a touch-friendly slider (0 to session length) with quick presets — Full · ¾ · ½ · ¼. A visual timeline shows everyone's relative participation as colour-coded bars, plus a fairness strip showing each person's saving (or extra cost) versus a flat equal split.
| Method | Output |
|---|---|
| Text | Plain summary copied to clipboard — paste into any chat |
| Image | Branded receipt PNG via html2canvas (lazy-loaded; ~47KB gz, only fetched when you click) |
| Link | Base64-encoded state in a URL — opens with the same split for the recipient |
State auto-saves to localStorage on every change (debounced 250ms) and rehydrates on revisit. Shared URLs override saved state. A one-tap Clear and start fresh wipes everything without confirmation.
- 22px touch-friendly slider thumbs with grab/grabbing cursors
- Mobile-first 520px-max layout, glassmorphic cards, layered radial gradients
:focus-visibleoutlines on every interactive control- Mode picker is a proper ARIA
tablistwith arrow-key navigation - Sliders announce position via screen reader (
aria-valuetext="45m of 90m") - Toast announcements use
role="status" aria-live="polite" - Respects
prefers-reduced-motionfor count-up animations
| Layer | Choice | Why |
|---|---|---|
| UI | React 19 | Hooks, modern concurrent features, no extra deps |
| Build | Vite 8 | Sub-second HMR, ESM-native, automatic code-splitting for the export library |
| Motion | Framer Motion 12 | Layout transitions, count-up motion values, layoutId tab indicator, reduced-motion aware |
| Styling | Custom CSS via injected <style> |
No framework, design tokens via CSS vars, glassmorphism with backdrop-filter where supported |
| Icons | Lucide React | Consistent stroke-based set, tree-shakable per-icon import |
| Image export | html2canvas (lazy) | Dynamic import() — only fetched when the user clicks "Image"; drops 47KB off initial bundle |
| Hosting | GitHub Pages | Static, free, fits a no-backend SPA |
| Persistence | localStorage + URL params |
Versioned key (splidit:state:v1), debounced writes, base64 URL state for sharing |
SplitFare/
├── docs/ # built output — served by GitHub Pages at splidit.co.uk
│ ├── assets/
│ ├── index.html
│ ├── favicon.svg
│ ├── og.html # OG card template (screenshot to produce og.png)
│ ├── robots.txt
│ └── sitemap.xml
└── splidit/ # source
├── public/
│ ├── favicon.svg
│ ├── splidit.png # icon / favicon
│ ├── og.html # OG card design (1200x630 screenshot template)
│ ├── robots.txt
│ └── sitemap.xml
└── src/
├── lib/
│ ├── calc.js # pure split and settlement helpers
│ └── calc.test.js # unit tests
├── App.jsx # full app — components, CSS, logic
├── index.css # base reset
└── main.jsx # React entry
A deliberate single-file component model: all the CSS, helpers, and components live in App.jsx so the whole app reads top-to-bottom. ~2,200 LOC at v1.0.
dist/assets/index.js ~120KB gz ← React + Framer Motion + app
dist/assets/html2canvas.js ~47KB gz ← lazy-loaded on Image export only
- Node.js 18 or later
- npm
git clone https://github.com/OElhwry/SplidIt.git
cd SplidIt/splidit
npm install
npm run dev # → http://localhost:5173/npm run build # → ../docs (two chunks, ready for GitHub Pages)
npm run preview # local preview of the built bundlenpm run lint # ESLint with React-hooks and react-refresh rules
npm run build # production build (main + html2canvas chunks)
npm run preview # preview the prod build locally
npm test # run unit tests via Vitest| Stored | |
|---|---|
Your device only (localStorage) |
The split you are working on — people, amounts, durations, mode |
| In a URL (only if you share a link) | Base64-encoded state in ?data=... — never sent to any server |
| Never collected | Account info, analytics, telemetry, cookies, third-party trackers |
| Third parties | None — SplidIt talks to nothing. Static site, zero network traffic beyond the initial load. |
Reset everything with the Clear and start fresh button at the bottom of the app.
| Tool | Used for |
|---|---|
| Framer Motion | Page reveals on scroll, count-up balances, animated mode-tab indicator (layoutId), settled-up burst, slider springs |
| Lucide React | All inline SVG icons (no emoji, no icon-font weight) |
| Inter · JetBrains Mono | Sans and tabular-numerals monospace |
| Coolors | Emerald gradient palette tuning |
| GitHub Pages · Vite | Static hosting and build pipeline |
Brand identity: emerald #34d399 to deep #10b981 on a dark green-black #060c08 canvas. Cursor-follow glow, layered radial gradients, and a soft dot-grid mask provide depth without busyness. Dark-first by design — same family resemblance as the portfolio page.
This project — every line of JSX, the calculation logic, the CSS, the motion choreography, the share-card design, even this README — was built in collaboration with Claude (Anthropic) using Claude Code. The By Time mode, the polish pass that took the app from a basic form to a portfolio piece, and the engineering decisions around persistence, lazy-loading, and accessibility were all worked out in conversation.
- MV1 — Equal + Custom % modes · settlements algorithm · text/image/link share · GitHub Pages
- v1.0 — By Time mode with timeline · emerald revamp · Framer Motion choreography ·
localStoragepersistence · lazy-loaded image export · ARIA tablist mode picker · branded receipt card · first-visit pulse - v1.1 — Currency selector · multi-split history · keyboard shortcuts · per-person notes
- v2 — Sub-splits within a single bill · saved groups · per-person discount/tax adjustments · CSV export
- Time-based split — a mode designed for the specific friction of shared activity bookings (padel, tennis, hourly courts) that no general-purpose splitter handles cleanly
- Settlements algorithm — minimum-transactions greedy matching, not naive pairwise transfers
- No backend — works offline, no signup, no telemetry, no third-party calls; everything client-side
- Polish discipline — count-up numbers, sliding tab indicators,
layoutId-driven morphing, draw-in settlement arrows, springy thumbs, settled-state burst, first-visit pulse hint - Accessibility taken seriously —
aria-valuetexton sliders,role="tablist"mode picker with arrow-key navigation, live regions for toasts, focus-visible outlines, reduced-motion respect - Engineering signals — code-split bundle (initial 120KB gz, deferred 47KB gz), debounced versioned persistence, URL-state restoration, html2canvas-safe receipt design
Built as a portfolio piece · used to settle every group dinner since.