Farebox is a civic-tech concept: one app for a fictional city's buses, trams and metro. The site is plain Vite + vanilla TypeScript — no framework — and its two working parts are a hand-authored SVG network map and a seeded arrivals engine that ticks like the real thing.
Transit apps earn trust with plainness: the time means the time, the alert says what broke, the map admits which stations have stairs. So the design language is municipal-modern — ink on white, four line colours used as chips and strokes (never as body text), Vignelli's 45° map discipline — and the accessibility work isn't a checklist at the end. It's the product's whole argument, so the site makes it visible: an honest step-free section and a table of measured contrast ratios.
Line, Station, LineId) in src/data.ts;
the map, the arrivals engine and the station cards are all derived from it. Add a station to
the array and every view knows about it.The map is not an image. Four route paths (45° angles only, drawn with round joins over a white "casing" stroke so crossings stay legible) and 25 stations are built from the data model at runtime:
{ id: 'union', name: 'Union', x: 300, y: 300, lines: ['R','B'],
stepFree: true, access: 'Step-free everywhere, including between
platforms.', lx: 288, ly: 326, anchor: 'end' }
Every station is a real control — an SVG <g role="button" tabindex="0">
with a spoken label ("Union. Interchange, Red Line and Blue Line. Step-free.") and a generous
invisible hit circle. The line picker is a proper radiogroup; choosing a line dims the other
routes and their exclusive stations to 15% opacity, which reads as "highlighting" without a
single colour changing:
pick(line: LineId | 'all'): void {
for (const [id, g] of this.routeGroups)
g.classList.toggle('is-dim', line !== 'all' && id !== line);
for (const s of STATIONS)
this.stationGroups.get(s.id)!.classList
.toggle('is-dim', !(line === 'all' || s.lines.includes(line)));
}
On load, each path animates in with the classic stroke-dasharray trick, staggered
per line, then the station dots fade up — unless the visitor prefers reduced motion, in which
case the network is simply there.
There is no backend, but the departures board must not loop a canned GIF. Every
(station, line, direction) gets its own mulberry32 PRNG seeded from its name, and
departure gaps are drawn from the line's real headway range:
const rand = mulberry32(hash(`farebox:${stationId}:${lid}:${dir}`));
while (last < now + 3600) {
last += hmin + rand() * (hmax - hmin); // e.g. metro: 240–420 s
s.times.push(last);
}
The clock is the real clock; the sequences are deterministic. Times decay through the grammar
riders actually use — 4 min → 1 min → due — and when a departure passes, its row
leaves and the list re-sorts with a small FLIP animation (positions captured, DOM reordered,
deltas animated back to zero). A visually-hidden aria-live="polite" region
summarises the top three departures at most every 30 seconds, so a screen reader hears a
board, not a metronome.
#111418 on white is
18.5:1; secondary text 11.3:1; the whole body layer clears AAA's 7:1. The honest trade: pure
Amber Line paint (#f5a623) is 2.0:1 on white — invisible to low vision — so on
light surfaces the amber is drawn as #c47600 (3.5:1, passes non-text contrast)
and the bright amber only appears on dark surfaces, where it hits 9.1:1. The map is honest
about physics.Vite needs three things to live at /farebox-transit/ on Pages: the base path, an
output directory Pages can serve from the repo (docs/), and the guide page as a
second rollup input so /guide/ resolves statically:
// vite.config.ts
export default defineConfig({
base: '/farebox-transit/',
build: {
outDir: 'docs',
rollupOptions: { input: {
main: resolve(__dirname, 'index.html'),
guide: resolve(__dirname, 'guide/index.html'),
}},
},
});
npm run build # tsc --noEmit && vite build → docs/
git add -A && git commit
gh repo create USER/farebox-transit --public --source . --push
gh api --method POST /repos/USER/farebox-transit/pages \
-f 'source[branch]=main' -f 'source[path]=/docs'
A .nojekyll in public/ lands in docs/ and keeps Pages
from second-guessing the folder structure.
Farebox is a design-showcase concept. Calder is fictional; the arrivals are a seeded simulation, not GTFS-RT. The repository README maps exactly what is demo and what a production build would need.
← Back to Farebox