Your band's whole songbook — chords, lyrics and setlists — running offline on the phone clipped to your mic stand.
No signup. No install. Loads once, then works with zero signal.
User Guide · Features · Quick start · Save it to your phone · Tech stack · Architecture
Open a set → tap a song → hands-free auto-scroll → tap any chord for the fingering.
SetList69 is an offline-first web app for performing musicians. It stores your songs as lyrics with chords, groups them into setlists, and renders them in a large, high-contrast view built to be read while your hands are busy playing.
It's the app you actually run the gig from. It imports the library you already have — OnSong, ChordPro, OpenSong and plain text all come straight in — and then keeps everything on your device. No account, no subscription, no cloud, nothing to lose when the venue's Wi-Fi doesn't exist.
The whole thing is a single setlist69.html file. No framework, no build step, no node_modules. You can read every line of it, fork it, and host it anywhere.
On stage
- Auto-scroll that keeps your hands free — starts at a tap, adjustable speed mid-song, with a progress bar and a haptic nudge when you reach the last line.
- No chrome while you read — the control dock hides itself; tap the lyrics when you need it and it gets back out of the way.
- Never goes dark — the app holds a screen-wake lock the whole time it's open, so the screen won't sleep between verses, songs, or sets.
- Chord chart — flip on a strip of fingering diagrams for every chord the song uses, straight from the ⚙ sheet.
- Stage mode — hides every editing control so nothing destructive is one fat-fingered tap away, and leaving a song mid-performance deliberately takes two taps.
- Two columns on wide screens — on a tablet or a phone held sideways, verses flow into side-by-side blocks (each verse kept whole), so most songs fit with little or no scrolling.
- Swipe between songs — in set order during a gig, or straight through your library when browsing, position (
3/12) always visible. - 🎲 Spin — one tap opens a random song from your library, for practice nights when nobody can pick.
- ✓ Played marks — songs cross off behind you as you play them, and survive a reload or a backgrounded phone.
Chords and keys
- Chords sit on the right syllable — a custom rendering engine locks each chord above the exact syllable it changes on, wraps to your screen width, never splits a word, and never scrolls sideways.
- Transpose per song (remembered next time) or across a whole set at once, with a ♯/♭ spelling toggle.
- Capo support — set the fret and chords redraw as the shapes your fingers actually make.
- Tap any chord for a fingering diagram — open voicings plus movable barre shapes, always matching what's on screen after transpose and capo.
- Nashville numbers — flip chords to scale degrees for the players who read that way.
Your library
- Import what you already own — ChordPro (
.cho/.chordpro/.pro), OnSong (.onsong/.txt, includingKey:andCapo:), OpenSong XML, plain chords-above-lyrics, and ZIP bundles of any of those. - Paste from the web — a built-in helper opens the usual chord sites, and pasted text gets cleaned up and parsed automatically.
- Songs are shared, not copied — fix a typo once and every setlist using that song is fixed.
- Share a single setlist with a bandmate as one file that merges into their library instead of trampling it — matching songs are reused, not duplicated.
- Back up everything to plain, readable JSON that you own.
Offline and yours
- Works with zero signal — a service worker precaches every asset, fonts included, on first load.
- Installs to your home screen and launches like a native app. How →
- No server, no account, no telemetry. Your songs live on your device and move by file.
- Dark and light themes, both held at or above WCAG AA contrast, with real pinch-zoom and screen-reader labels throughout.
Open a song → ⚙ → Share this song (or long-press it in the library). You get a link with the whole chart — chords, sections, key, stage banter — packed into it. Text it, email it, AirDrop it. Whoever opens it is asked whether to add it to their library.
Nothing is uploaded anywhere: the song rides in the URL's #fragment, which browsers never send to
a server. There's no account and no backend to sync through.
On an installed iPhone/iPad app, open ≡ → Add a song from a link and paste the link there. Tapping a link opens Safari, and iOS gives an installed home-screen app its own separate storage — so a tapped link would land in a copy of the app you don't use.
No React, no Tailwind, no bundler — the runtime dependency count is zero. Everything below is either a browser API or a file served straight from this repo.
| Layer | What it uses |
|---|---|
| App | Hand-written HTML5, CSS (custom-property theming, flexbox/grid) and vanilla JavaScript (ES2017+), all in one setlist69.html |
| Storage | IndexedDB for the library with a localStorage fallback; state is structurally validated on load and persisted whole |
| Offline / install | Web App Manifest + a cache-first Service Worker precaching every asset; the Wake Lock API keeps the screen alive through a set |
| Files / sharing | File System Access API, Web Share API and an anchor-download fallback, chosen per platform at runtime |
| Import | ChordPro, OnSong and OpenSong XML parsers (the last via DOMParser); ZIP bundles unpacked with the native DecompressionStream |
| Fonts | Self-hosted WOFF2 (JetBrains Mono, Hanken Grotesk) — no CDN, so text renders identically offline |
| Dev tooling | Node.js --check for syntax, Playwright for the screenshots and hero GIF, GitHub Actions for CI, GitHub Pages for hosting — none of it ships to the browser |
Just want to use it? You don't need any of this — open the live app and you're done. This section is for running it locally or hosting your own copy.
| For | You need |
|---|---|
| Using the app | Any modern browser. That's it. |
| Running it locally | git |
| Regenerating the screenshots / hero GIF | Node.js ≥ 18 and Playwright (optional) |
git clone https://github.com/cdburgess75/SetList69.git
cd SetList69Nothing to install. There's no build step, no package manager and no dependencies — the app is static files.
The only optional install is the dev tooling that regenerates the visuals in this README:
npm install playwright gifenc pngjs # optional — screenshots and hero GIF onlyOpen setlist69.html in your browser directly, or serve the folder so the service worker and offline caching behave the way they will in production:
npx serve .
# → http://localhost:3000Push the repo to any static host — GitHub Pages, Netlify, Cloudflare Pages, or a folder on your own server. There's no build to run and nothing to configure. (.nojekyll is already in place so GitHub Pages serves the files as-is.)
SetList69 installs to your home screen and runs fullscreen like a native app — no App Store, no account. This is how you'll actually want it at a gig.
- Open cdburgess75.github.io/SetList69 in Safari.
- Tap the Share button (the square with the arrow pointing up).
- Scroll down and tap Add to Home Screen.
- Tap Add.
It has to be Safari — Chrome on iOS can't install home-screen apps.
- Open cdburgess75.github.io/SetList69 in Chrome.
- Tap the ⋮ menu (top right).
- Tap Install app — or Add to Home Screen on older versions.
- Tap Install.
Chrome often offers this on its own with an install banner along the bottom of the screen.
Click the install icon (a monitor with a downward arrow) at the right-hand end of the address bar, then Install.
- Launch it once while you still have signal. That's when it caches everything.
- From then on it opens fullscreen and works with no connection at all — airplane mode, basement venue, dead rural bar, doesn't matter.
- When a new version ships, the app slides down an "Update ready" banner naming it and keeps a ⟳ button in the header to apply it. It never reloads itself mid-song.
Two columns on tablets — wide screens flow verses into side-by-side blocks automatically, so the whole song fits with little or no scrolling
SetList69/
├── setlist69.html # The entire application — HTML, CSS and JS in one file
├── sw.js # Service worker: cache-first, precaches every asset
├── manifest.json # PWA manifest (id, icons, screenshots, standalone)
├── index.html # Redirect stub → setlist69.html
├── .nojekyll # Tell GitHub Pages to serve the files as-is
├── fonts/ # Self-hosted WOFF2 (Hanken Grotesk, JetBrains Mono)
├── icons/ # App icons incl. an Android-maskable variant
├── docs/
│ ├── USER-GUIDE.md # End-user walkthrough
│ ├── DEVICE-TESTING.md # Manual test pass for touch/visual behaviour
│ ├── shots.js # Playwright helper — regenerates the screenshots
│ ├── hero-gif.js # Playwright helper — regenerates the hero GIF
│ └── screenshots/
└── .github/workflows/
└── check.yml # CI: syntax, version-match, duplicate-id, manifest checks
Inside setlist69.html the code reads top to bottom: persistence (IndexedDB + localStorage fallback) → seed data → music core (transposition, chord detection) → parsing and rendering engine → screen router → list renderers → import/export → event wiring.
A single in-memory state object holds everything and is persisted whole:
state = {
songs: [{ id, title, sub, banter, key, capo, defaultTranspose, body }],
setlists: [{ id, name, notes, setTranspose, songIds: [...] }],
theme: "dark"
}Songs are a shared master store and setlists reference them by id, so editing a song updates every set that uses it. Deeper notes — the rendering pipeline, capo maths, merge-import rules — live in CLAUDE.md.
There's no unit-test framework by design. Correctness is enforced by CI checks plus small targeted Node harnesses when the music core changes.
# What CI runs on every push — syntax-check the extracted script and the worker:
node -e "const s=require('fs').readFileSync('setlist69.html','utf8').match(/<script>([\s\S]*?)<\/script>/)[1];require('fs').writeFileSync('/tmp/app.js',s)" \
&& node --check /tmp/app.js && node --check sw.js
# Regenerate the README visuals after a UI change (needs Playwright + Chromium):
node docs/shots.js # the six screenshots
node docs/hero-gif.js # the animated hero
# Touch and visual behaviour has to be checked on real hardware:
# → follow docs/DEVICE-TESTING.mdCI (.github/workflows/check.yml) also fails the build on a version mismatch, duplicate element ids, or a manifest referencing a missing precached file.
Revisions use vYYYY.MM.DD.NNN. Every app change bumps the version in three synced places — the changelog comment at the top of setlist69.html, the on-screen brand tag, and the CACHE constant in sw.js. CI fails the build if they drift. The full changelog lives at the top of setlist69.html.
A personal tool developed in the open. Issues and PRs are welcome:
- Read
CLAUDE.mdfirst — it documents the architecture, the rendering engine's invariants and the change checklist. - Keep the constraints: vanilla JS, one file, zero runtime dependencies, no build step.
- Bump the version in all three places (CI will catch you if you don't).
node --checkthe extracted script, and add a small Node harness if you touch the music core.- Flag anything needing real-device testing (touch, share sheet, install flow) in your PR description.
MIT — use it, fork it, self-host it, gig with it. Attribution is the only condition.
Built for working bands. No cloud, no subscription, no ads — just your songs.





