Design showcase · build notes

A transit map you can press.

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.

The idea

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.

The stack

Signature technique #1 — the octilinear SVG map

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.

Signature technique #2 — the seeded arrivals engine

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.

Details that matter

Ship it on GitHub Pages

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