FIFA World Cup 2026™ companion app — live scores, standings, bracket, squad explorer, player profiles and global timezone support. A premium glassmorphism PWA with no login, no account, no tracking.
Live: https://wc26.ajithmjose.com
Most World Cup apps are either too basic or bloated with accounts, ads and slow load times. This was built to be the app we actually wanted to use during the tournament — fast, beautiful, works offline, no login required.
Core principles:
- Every piece of information a fan needs is reachable in two taps or less
- Works fully offline — all fixtures, teams and squad data are bundled
- No account, no friction — favourites and alerts stored locally
- Data is always the same shape regardless of source (live or static)
- Full schedule of all 104 matches — group stage through the Final
- Date rail — horizontal scrollable day navigation, auto-scrolls to today
- Live carousel — swipeable cards of all in-progress matches with live score and minute
- Countdown card — live ticking countdown to the next kickoff
- Match cards — score is always the largest element; long team names show their 3-letter FIFA code
- Status indicators — NS, LIVE, HT, FT, PST, CANC
- Filters — by status, team or group
- Searches 48 teams and 1,248 players simultaneously — all local, zero network calls
- Full official names from FIFA: "Lionel Messi", "Kylian Mbappe", "Cristiano Ronaldo"
- Tapping a player result opens their team profile and player drawer directly
- Desktop: centered spotlight modal; Mobile: full-screen overlay
- Full-screen page per team with sticky tab navigation (Matches · Squad · Standings)
- Squad tab — 26 players with position filters (Goalkeepers · Defenders · Midfielders · Forwards)
- Standings tab — full group table with the selected team highlighted
- Favourite toggle from the team page header
- Tap any player to open a bottom drawer showing: photo, name, jersey number, position, club
- Stats: International Caps · International Goals · Age · Height · Weight
- Data sourced from FIFA World Cup 2026™ official records
- All 12 groups (A–L): MP · W · D · L · GD · Pts
- Top-2 qualify indicator; accent highlights for leaders
- Full R32 → R16 → QF → SF → Final tree
- Placeholder slots auto-fill with real teams as the tournament progresses — no manual update needed
- Desktop: horizontal bracket with connector lines
- Mobile: round-by-round tab navigation
- Star any of the 48 nations to follow their matches
- Favourite matches grouped by day in the Favourites tab
- Persisted across sessions — no account needed
- Set a push reminder 15 min, 30 min or 1 hr before any match
- Notifications delivered via Firebase Cloud Messaging — fire even when the app is closed or the screen is off
- GOAL, KICKOFF, HALF TIME, FULL TIME push events for every live match
- Google Calendar — one-tap adds the match as a calendar event
- Undo support when removing alerts
- Alerts view with upcoming / past tabs
- 25-city curated list covering all major football markets
- Auto-detects browser timezone on first visit
- Every time surface updates instantly when timezone changes
- Works fully offline — all data bundled statically
- Installs on Android (Chrome) and iOS (Add to Home Screen)
- Auto-updates silently in the background
Building a new screen or component follows a deliberate order. Skipping steps produces the generic look this project was designed to avoid.
1. Start with the background, not the surface
Every view floats on var(--grad) — the radial gradient defined in tokens.css. Design with the background visible. Never design on white or a flat color.
2. Apply .glass to every surface
Cards, sidebars, drawers, modals, pills — all use the .glass primitive: backdrop-filter blur + translucent rgba fill + top edge-light + hairline border + soft shadow. This is defined once in app.css. Never recreate it inline, never use an opaque card with a solid border.
3. Use tokens, never hardcode
All colors, radii, spacing, and blur values come from src/styles/tokens.css as CSS custom properties. Components only reference variables — never hex codes or pixel values directly.
4. Score and status first — hierarchy drives layout On any match surface, score is the largest element. Status (NS, LIVE, HT, FT) comes next. Team names are secondary. Build the hierarchy before adding decoration.
5. Color carries meaning, nothing else
- Tangerine
#FF6A2B— interactive, accent, brand - Red
#FF4D5E— live match state only - Gold
#FFC23D— standings rank only - Everything else inherits from
--text-*and--surface-*tokens
Dual theme is handled by CSS vars alone — no conditional class logic in components.
6. Motion last, not first
Framer Motion is added after layout and hierarchy are correct. Page transitions are native-feeling pushes. Drawers use spring physics (type: "spring", tight damping). Card lists stagger on mount. If motion is distracting, reduce it — never add it to fill empty space.
7. Mobile is the real canvas
Design at 390px. Desktop (lg breakpoint) adds sidebar and right rail as enhancements — not a separate layout. Every feature must work on mobile. Bottom TabBar with safe area inset; no hover-only interactions.
8. Icons and flags are always the same source
Icons: src/components/Icon.tsx with inline SVG only. Flags: src/components/FlagImg.tsx via flagcdn.com. Never introduce an icon library or bundle flag assets.
- Accent tangerine
#FF6A2B— distinctive and energetic. Every other football app uses green. - Live red
#FF4D5E— used exclusively for live match indicators - Gold
#FFC23D— standings rank highlights - Full dual theme (light + dark) via CSS custom properties — no conditional class logic in components
- Geist variable font only — no mixing, no serif
- Tabular figures on all scores, times and stats so numbers never shift layout
- Page transitions feel like native pushes
- Bottom drawers use spring physics — snappy, not floaty
- Match cards stagger in as you scroll
- Designed for mobile. Desktop adds a sidebar and right rail as enhancements — no features are desktop-only
- Bottom TabBar with safe area support; sidebar on desktop
All icons are inline SVG — no external font, no extra request, no license.
Static JSON seed data (fixtures, teams, squads) is bundled with the app at build time. Live scores overlay on top of the seed via a Firestore onSnapshot listener — all views consume a unified data model and never branch on whether data is live or static.
A VPS poller fetches live scores every 20 seconds during active matches (300 seconds when idle) and writes diffs to a single Firestore document. One document means one Firestore read per connected user per match update — cost doesn't grow with the number of live matches or users.
Push notifications are delivered via FCM topics — each user is subscribed to a topic per match they follow. GOAL, KICKOFF, HT and FULLTIME events are broadcast to all subscribers in a single FCM call regardless of subscriber count. Reminder pushes are sent directly to each device token at the user's chosen time. All push logic runs server-side — the browser never polls.
| File | Contents |
|---|---|
src/data/fixtures.json |
104 matches — UTC kickoffs, venues, groups, rounds |
src/data/teams.json |
48 teams — ISO code, FIFA rank, color, FIFA code, group |
src/data/squads.json |
48 × 26 players — name, jersey, position, age, caps, goals, club, height, weight, photo |
src/data/stadiums.json |
16 venues — city, country, capacity |
| Layer | Technology |
|---|---|
| Frontend | React 18 · TypeScript · Vite |
| Styling | Tailwind CSS · CSS custom properties · Framer Motion |
| State | Zustand — all slices persisted to localStorage |
| Routing | React Router v6 |
| Live data | Firebase Firestore |
| Push | Firebase Cloud Messaging (FCM) |
| PWA | vite-plugin-pwa · Workbox |
| Hosting | Firebase Hosting |
Create .env in the project root:
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=
VITE_FIREBASE_VAPID_KEY=
npm install
npm run dev # development server
npm run build # production build
firebase deploy --only hosting- No user authentication or personal data collected
- Favourites and alerts in
localStorageonly — user-controlled - FCM tokens stored in Firestore with no PII attached
- All sensitive keys gitignored — never committed
This repo includes a CLAUDE.md — a machine-readable instruction file for AI coding assistants (Claude Code, Cursor, Copilot, etc.). It documents:
- The full stack and every hard constraint
- The design system rules (glassmorphism, tokens, typography)
- The exact file layout with every component, hook, store slice, and view
- All routes, localStorage key namespaces, and feature flag states
- Component authoring pattern and commit conventions
If you use AI assistance on this codebase, point your assistant at CLAUDE.md first. It is kept accurate and up to date as the codebase evolves.
- Fixtures & squads: FIFA World Cup 2026™
- Player stats: Wikipedia
- Flags: flagcdn.com
- Font: Geist by Vercel
© 2026 ajithmjose