Games templates

Full design ยท Games

Quiz app

A trivia game and quiz maker with real game feel.

Live preview. Click around, it works.

About this template

A trivia game and quiz maker with real game feel. Players pick a category, race a timed round with streak multipliers, and take a daily challenge that is the same puzzle for everyone that day; a result screen builds a generated share card and a personal stats screen tracks accuracy and best streaks over time. Switch to Viewing as host to build a quiz, manage a question bank tagged by difficulty that adapts to how the player is answering, and open a host screen with a room code for a live group round. Tactile answer cards, a draining ring timer, optional sound and keyboard 1 to 4 throughout.

What the agents are asked

The first message sent when you use this template. You can edit the plan before anything is built.

Adapt this trivia game to the customer's brief: keep the timed round, streak scoring, daily challenge, share card and stats screens exactly as they work, and keep the host side's quiz builder, difficulty tagged question bank and room code host screen. Rename the brand, recolour the identity and rewrite the copy, and replace the sample question bank with original questions for their subject and audience, keeping the same difficulty tags so the adaptive round keeps working. Where they want a real live game, add accounts and the tables in schema.sql, and accept scores only through the server function the README names, never a direct write to a score table.

A trivia game and quiz maker. Players pick a category and race a timed, six question round whose difficulty follows their own streak; everyone gets the same daily challenge on the same day; results end in a generated share card, shown in the page, never downloaded. Hosts sign in to build a quiz from an original question bank, tag each question's difficulty, and open a demo live screen with a room code for a group game. "Klaxon" is a fictional brand; rename it freely.

  • Plain HTML, CSS and one ES module. No dependencies, no build step, no external JavaScript.
  • Built on the NovaBuild template kit (docs/guardrails/TEMPLATE-KIT.md): CONFIG at the top of script.js holds the brand, contact details, locale and feature flags; the design tokens block at the top of styles.css holds every colour, font, radius and spacing; the data layer runs in demo mode (localStorage) by default and against a real Supabase project once CONFIG.backend is filled in, with no other code change.
  • Mobile first: a bottom tab bar and a two column desktop layout from 820px wide.
  • One committed dark look (not a light and dark toggle): navy is the ground on every screen, parchment is used for on screen text and for the pale card surfaces (answer cards, quiz cards), and one raspberry accent is spent on the timer's last seconds, a correct answer's flash, streaks and every primary action.
  • Everything is saved in the browser (localStorage) through store, the shared kit object. Every access is wrapped in try/catch, so the sandboxed catalogue preview (no storage at all) and a locked down browser both still run.
  • Sample rows carry SAMPLE_VERSION. On load, if the stored version is stale, only rows tagged is_sample: true are replaced; every row a visitor made themselves is kept.

Art direction

Idea. A pub quiz night crossed with a game show buzzer: a dark stage, one confident pink accent, and a condensed marquee display face for scores and headlines.

Palette. Ink navy #12181F (the ground everywhere), Parchment #F4EFE2 (on screen text and every card surface), Raspberry #E23E74 (the one accent: the buzzer flash, the streak flame, the timer's last quarter, every primary button). Two small semantic tones sit alongside the identity for answer states only, never used as brand colours: a clean green for a correct answer and a warm red for a wrong one.

Type. League Gothic (display: headlines, scores, the room code) and Sora (text). Both from Google Fonts. Body line height 1.6, uppercase tracked headlines for the marquee feel.

Photography. Games get game feel instead of a full photo shoot (TEMPLATES.md, "The premium bar"): one cover image and one ambient texture, generated from media.json into public/template-media/quiz-app/.

NameWhere
cover.webpThe Home screen hero (a raspberry arcade buzzer, a hand about to press it), and og:image
glow.webpAn ambient stage light texture behind the room code card on the Live host screen

Every image has a CSS fallback colour so the layout holds while it loads.

Motion, one job each, all behind `prefers-reduced-motion`.

  • The signature moment, "the buzzer drop": pressing an answer punches the card down and springs back with no bounce; a correct answer sends one klaxon flash across the question card while the streak digit pops; the timer ring's last quarter turn flashes to the accent colour.
  • Page load: a staggered entrance on the Home headline, once.
  • Screen changes: document.startViewTransition, skipped and never left unhandled under reduced motion or a hidden tab (.ready.catch, L34).
  • Scroll: animation-timeline: view() reveals inside @supports, an IntersectionObserver fallback at threshold 0.2, and a forced, unconditional fallback class (html.reveal-fallback) for a hidden tab or a browser with neither (L24, L36).
  • Buttons press down like keys; hovers are 150 to 200ms, eased, no bounce.
  • Optional WebAudio sound (off by default): short synthesised tones for a correct answer, a wrong one and a streak. No audio files.

Screens

The hash router serves the player side by default and the host side once "Viewing as: Host" is selected (gated by CONFIG.features.hostMode).

Player side

  • Home (#/): the headline and hero image sit in the first screen; the day's stat strip, the daily challenge card (CONFIG.features.dailyChallenge) and the category grid.
  • Play (#/play/category/:slug, #/play/daily, #/play/quiz/:id): one question at a time, a draining ring timer (20 seconds), tactile answer cards, keyboard 1 to 4, an explanation after each answer, and a streak counter. Category rounds adapt: two correct answers in a row raises the difficulty for the next question, a wrong answer eases it back down. The daily challenge and a host's quiz play the same fixed set for everyone instead.
  • Results (#/results): the score, accuracy and best streak, a generated share card drawn on a <canvas> (verified by looking at it in the page; nothing is ever offered as a file download), and, when signed in, the round is saved through submit_round (below).
  • Stats (#/stats): totals, accuracy, best streak, a per category accuracy breakdown, a daily streak, the twelve most recent rounds, sound and "remove sample data" settings.

Host side ("Viewing as: Host")

  • Overview (#/host): your quizzes and any public one, a "New quiz" action.
  • Quiz builder (#/host/quizzes/:id): title, description, an optional category tag, public or private, the ordered question list (move, remove), and a picker onto the question bank filtered by category and difficulty. A "Preview" button plays the quiz through the same round engine as a category round.
  • Question bank (#/host/questions): browse and filter every question by category and difficulty, and add an original question with four answers, the correct one marked, a difficulty and an optional explanation.
  • Live host (#/host/live/:id): a room code, a demo roster of players "joining", and a step through the quiz's questions with a simulated answer distribution. This screen is a demo: it has no real second device syncing. See "What is sample or simulated" below.

Data model and score integrity

schema.sql holds players, categories, questions, quizzes, quiz_items, daily_challenges and rounds (a finished round doubles as the score log), row level security, explicit grants and the same sample rows the demo shows.

The rule that shapes rounds: a browser can never write a score directly. rounds has no insert, update or delete grant for anon or authenticated; only submit_round, a security definer function, can write it, and it never trusts a number the browser sends:

  1. The player is looked up from auth.uid(), never from an argument, so nobody can post a score as someone else (docs/ops/LESSONS.md L35).
  2. Calling it twice with the same client_round_token returns the first result both times; it never grades or scores a round twice.
  3. Every submitted question id is checked against the claimed category, quiz or, for the daily challenge, against get_daily_challenge(date): which locks in that date's six questions (the six lowest fnv1a32(id + date) hashes across the whole bank) the first time anyone asks for it, so a browser cannot submit a different, easier set and call it "today's challenge".
  4. Every answer is graded against the real questions.correct_index, and the score itself is computed from the question's real difficulty, the player's real streak entering that question and their own reported time (clamped to the 20 second limit), never from a number the browser supplied directly.

script.js's demo rpc.submit_round mirrors this exactly against localStorage, including the double submission check. Removing sample data is one delete (a sample player), which cascades in Postgres to that player's sample quizzes, quiz_items and rounds; the demo driver, which has no foreign keys, tidies up the same three tables to match.

What is sample or simulated

PartStatus
The 48 question sample bank, across 6 categoriesReal, original questions written for this template; kept when sample data is removed, the same way a menu or a glaze list is kept in other templates, not treated as removable demo people
"Sample player" and their quiz, rounds and statsSample, tagged is_sample, removed in one step from Stats
The daily challengeReal: get_daily_challenge locks in the same six questions for everyone that date
The share cardReal, drawn on a <canvas> from the actual result; never offered as a download
Live host's room codeReal, generated client side
Live host's "players joining" roster and answer barsSimulated for preview; the page says so
SoundReal, generated with WebAudio oscillators; no audio files
PhotographyGenerated for this template, served from NovaBuild at /template-media/quiz-app/
StorageThis browser only, until CONFIG.backend.mode is "supabase"

Connecting a real backend

Set CONFIG.backend to { mode: 'supabase', url: '...', anonKey: '...' } (the public anon key; never a service role or secret key, which the kit and the checker both refuse). Apply schema.sql to that project (additive, safe to run twice). Sign in becomes real Supabase Auth instead of the demo's "any email" shortcut; everything else, including submit_round and get_daily_challenge, is already implemented as real Postgres functions, not a stub to build later.

To add real live sync to the Live host screen, the natural next step is Supabase Realtime (or a similar channel): broadcast the current question index from the host, and have each joined player's device post its own answer through a new, equally careful security definer function (look up the player from auth.uid(), check the room is still open, refuse a second answer to the same question) rather than writing to a table directly.

Files

FileWhat it holds
index.htmlShell: skip link, banner, topbar, tabbar, the sheet dialog and toast containers
styles.cssTokens, reset, layout, the round and results screens, host screens, motion
script.jsConfig, the kit, sample data, the round engine, rpc, the router, every screen, events
media.jsonThe two images to generate (node tools/generate-imagery.mjs --template quiz-app)
schema.sqlTables, RLS, grants, submit_round, get_daily_challenge, daily_leaderboard, sample rows, a VERIFY block to rehearse
template.jsonCatalogue metadata and identity for NovaBuild

Accessibility and motion

Skip link; aria-current-style active states on every nav link and tab; labelled inputs throughout; visible focus rings; the timer ring and share card carry an accessible name (role="img", aria-label); the round announces its explanation in the page rather than a popup; confirmations happen in the page, never alert, confirm or prompt. Reduce motion removes the staggered entrance, the buzzer flash keyframe, the press spring and every scroll reveal, and shows every screen at rest instead.