Games templates

Full design · Games

Word guess

A polished daily five letter word game with its own look: a six by five board with tiles that turn to reveal each letter, an on screen and physical keyboard, hard mode, unlimited practice, stats with streaks and a...

Live preview. Click around, it works.

About this template

A polished daily five letter word game with its own look: a six by five board with tiles that turn to reveal each letter, an on screen and physical keyboard, hard mode, unlimited practice, stats with streaks and a guess distribution chart, a spoiler free share grid, friend puzzles sent as a link, and settings for high contrast, theme and reduced motion.

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 daily word game to my brand. Keep the daily puzzle, practice mode, stats, share grid, friend puzzles and settings, but change the name, colours, fonts and copy to suit my audience, and adjust the word lists if my players need a different vocabulary or theme. Keep it fast on a phone, keep the keyboard and screen reader support, and keep the share grid free of letters so it never spoils the answer.

A daily five letter word puzzle with its own identity. Everyone gets the same word each day, has six tries to find it, and each guess turns its tiles to show which letters are Set (right letter, right place), Near (in the word, somewhere else) or Out (not in the word). Around the daily puzzle: unlimited practice, stats with streaks and a guess distribution chart, a spoiler free share grid, friend puzzles sent as a link, and settings for hard mode, high contrast, theme and reduced motion.

"Tessera" is a made up brand for the template (a tessera is one tile of a mosaic); rename it freely. The game genre is a well known one, but the name, palette, tile vocabulary and reveal animation here are its own.

Files

FileWhat it is
index.htmlShell: top bar (brand, top navigation on desktop), main, bottom tab bar on phones, the share dialog, a toast region and a screen reader announcer.
styles.cssTokens first (light, dark, high contrast), then layout and components. Every colour is a custom property.
script.jsOne plain ES module, no dependencies. Word lists, daily schedule, store, game engine, views and hash router.
schema.sqlPostgres tables, row level security, grants, a leaderboard function and sample rows for the signed in version.
template.jsonCatalogue entry for NovaBuild.

No build step. Serve the folder with any static server (Vite inside NovaBuild, or any local static server).

Screens

RouteScreen
#/Today. Once a day, before the first guess, a full screen lapis cover over the stone texture photograph: the wordmark laid in tiles, a one line pitch, Play, How to play, Practice, and the puzzle number, date and streak. Play opens the board (a view transition where supported). The board screen: puzzle number and date (on desktop a sticky rail with a very large number and the streak, board and keyboard to the right), the 6 by 5 board and the on screen keyboard (a physical keyboard works too). An invalid word shakes the row and says why. When the game ends the keyboard gives way to a result panel: a mosaic medallion showing the guess count on a win, or the answer in tiles on a miss, then a countdown, Share, Stats and Practice. First visit shows a dismissible "New here?" note.
#/practicePractice. Unlimited random words from the answer list (never today's word, and no repeats of the last 40). Separate stats, never touches the daily streak. "New word" after each game.
#/howHow to play. The cover photograph (a clip reveal), sticky beside the text on desktop and a wide band on top on phones. Numbered steps, three example rows whose marked tile flips to its colour as it scrolls into view, the ways to play, keyboard and sound.
#/statsStats. Daily or Practice. Played, win %, current streak and max streak as four solid stone blocks (black, lapis, terracotta, limestone); guess distribution drawn as runs of tesserae, at most 14 per bar, scaled to the longest (today's bar in lapis); a lapis countdown block with Share today's result; a demo "Friends, last 7 days" leaderboard with sample players, each marked Sample.
#/createCreate a puzzle. Two columns on desktop. Pick any five letter word (it is mirrored into tiles that pop as you type) (a word outside the list needs a tick to confirm), add an optional "From" name, get a link #/p/<code> on a postcard with a laid tile strip, a Copy button and confirmation in the page. Lists the puzzles you have made with Copy link, Try it, and Remove (asks again in the page before removing).
#/p/<code>Friend puzzle. Plays the decoded word on a fresh board, titled "A puzzle from Sam". A damaged link shows a clear message instead of a broken board.
#/settingsSettings. Hard mode, high contrast, sound (off by default), theme (match device, light, dark), motion (match device, reduce, full), display name for the leaderboard, remove or restore sample games, clear everything (confirmed in the page).
Share (dialog)Opens from the result panel or Stats. Shows the grid as coloured squares, copies a text version with emoji squares and no letters, confirms "Copied" in the dialog. If the browser blocks the clipboard, it shows the text selected for manual copying. Emoji appear only in that copied text.

Game rules as built

  • Guesses must be in the allowed list (2,970 words) or be the answer itself. Repeated guesses are refused.
  • Scoring handles repeated letters: a letter is marked Set or Near only as many times as it appears in the answer.
  • Hard mode is fixed for a game at its first guess. Set letters must stay in place and Near letters must be reused (as many times as they were revealed). Messages say exactly what is missing, for example "3rd letter must be R".
  • High contrast swaps to saffron (Set) and cobalt (Near) and adds a bar under Set tiles and a dot on Near tiles, so colour is never the only signal. It also changes the squares in the share text.
  • Reduced motion follows the device by default. When on, tiles colour instantly with no turn, shake, lift, falling tiles or screen transition, and the medallion appears already laid.
  • Sound is off by default. When on, WebAudio synthesises a stone tap per key, a click and a pitch per revealed tile (higher for Set, lower for Out), a low double knock on a refused word and a short chime when you solve it. No audio files; a browser without WebAudio stays silent.
  • Screen readers: each tile is labelled with its letter and state, each guess is announced after it lands, and the keyboard keys carry their state in their labels.

The words and the daily schedule

script.js holds two lists near the top:

  • ANSWER_WORDS: 581 common, everyday words. The daily word and practice words come only from here.
  • EXTRA_WORDS: 2,389 more words accepted as guesses but never chosen as answers (plurals, past tenses, less common words, and US spellings such as COLOR so guesses are not refused).

The daily word is chosen deterministically: the answer list is shuffled once with a fixed seed, and puzzle number N (counted from 1 January 2026, in the player's local time) takes the Nth word, wrapping after 581 days. Everyone on the same calendar day sees the same word, with no server. Changing the answer list reshuffles every future day, so settle the vocabulary before launch. To keep today stable after launch, append new answers to a second list and pick from it only for dates after the change, or move the schedule to the daily_answers table.

Friend puzzle links

encodePuzzle(word, from) turns the word into six letters (each letter shifted by a per position amount, plus a check letter that catches damaged links) and appends the optional name in base64url: #/p/tyclvo.TWFyYQ is MAPLE from Mara. This keeps the word out of sight at a glance; it is not encryption, and the Create screen says so. For secret puzzles, store the word server side (custom_puzzles) and check guesses through a server function.

Data model

One object, store, is the whole data layer. Its state:

sampleVersion   bumped when the seeded sample games change shape           (device only)
player          { id, name, createdAt }                                   -> players
settings        { hard, contrast, theme, motion, sound }                   (device only)
games[]         { id, mode: daily|practice|custom, key, answer, code?, from?,
                  guesses[], guessCount, status: playing|won|lost,
                  hard, sample, startedAt, finishedAt }                    -> games (finished only)
puzzles[]       { id, code, word, from, createdAt }                        -> custom_puzzles
recentPractice  last 40 practice words, to avoid repeats                   (device only)
seenHelp        whether the first visit note was dismissed                 (device only)
coverDay        the day the cover was last passed with Play               (device only)

key is the date (2026-09-23) for daily games, a random id for practice games, and the six letter code for friend puzzles. Everything is saved to localStorage under tessera.word-guess.v1, every access wrapped in try/catch so a blocked storage still plays for the visit.

What is sample or simulated

  • Sample daily history. Four weeks of sample daily results (26 games, 24 wins, current streak 6, max 9), dated relative to today and flagged sample: true, so Stats looks alive on first open. Stats labels them "Includes sample games" with a one step "Remove sample games" button; Settings can remove or restore them. The stored sampleVersion is checked on load: when it does not match the version the script seeds now, the sample games are replaced with freshly generated ones and every game the visitor played themselves is kept.
  • Demo leaderboard. Mara, Theo, Priya, Jonah and Ruby are sample players, each marked Sample, with fixed results for the last seven days. "You" is computed from your real (and sample) daily games. The same players and results are seeded by schema.sql.
  • Nothing is sent anywhere. There is no account, no server and no tracking.

Adding accounts and a leaderboard

  1. Auth. Add Supabase Auth (email link or a social provider). After sign in, insert a players row with user_id = auth.uid() and the chosen display name if one does not exist.
  2. Apply `schema.sql`. It is additive and safe to re-run. Before applying to a live project, rehearse inside a transaction as a real signed in user and as anon, and roll back:

``sql begin; -- paste schema.sql select set_config('request.jwt.claims', '{"sub":"<real user id>","role":"authenticated"}', true), set_config('role', 'authenticated', true); -- should fail or return nothing: reading another player's games, inserting a game for another -- player_id, inserting is_sample = true, selecting from daily_answers, updating a game. -- should work: inserting your own finished game, calling daily_leaderboard(current_date - 6, current_date). rollback; ``

RLS refuses updates and deletes silently (zero rows, no error), so count rows in every refusal test.

  1. Swap the store. Only store changes. Keep localStorage as the offline copy and add writes through:
  2. load(): after sign in, fetch players (own row), games (own rows) and custom_puzzles (own rows) and merge them with anything on the device, so a streak started signed out is kept.
  3. addGuess(): when status becomes won or lost, insert one games row (mode, puzzle_date for daily, puzzle_code for custom, guesses, guess_count, solved, hard_mode). Rows are never updated: a finished game is written once.
  4. addPuzzle() / removePuzzle(): insert into and delete from custom_puzzles.
  5. setPlayerName(): update players.display_name.
  6. leaderboardRows() (in the Stats section): call rpc('daily_leaderboard', { p_from, p_to }) instead of using SAMPLE_FRIENDS, and drop the Sample labels.
  7. A leaderboard nobody can game. The version above trusts the browser to report results honestly, which is fine for friends. For a public board, move the answer off the device:
  8. Fill daily_answers ahead of time from ANSWER_WORDS (a server script with the service role key).
  9. Add a server function (a Supabase Edge Function such as submit-guess) that receives the player's guess, checks it against daily_answers with the service role, returns the Set, Near, Out pattern, and writes the games row itself when the game ends. Then revoke insert on games from authenticated.
  10. Remove dailyAnswer() from the browser for daily play; score() then runs on the server.
  11. The service role key lives only in the server function's secrets. It never reaches the browser.
  12. Friends only boards. daily_leaderboard ranks everyone who played. For friend groups, add a friendships (or groups and group_members) table with its own RLS, and filter the function by the caller's group.

Art direction

A lapis and terracotta stone mosaic, with NYT Games' bold colour as the benchmark: one strong band colour per mode and a clear tile identity, not a quiet page.

Palette. Three solid colours on a cool lime plaster ground (#EDEEEA), never cream:

TokenHexUsed for
Lapis#1B4E9EThe brand, the daily band and cover, Set tiles, primary buttons
Terracotta#C0502ANear tiles, the practice band, eyebrows and step numbers
Grout black#1A1C22Ink, the friend puzzle band, black stone in the medallion

Out tiles are #5E6168. Dark theme: a night lapis ground #0D1424 with a brighter lapis #2F66C4 for Set tiles. Terracotta is #C0502A rather than the #C8552B first proposed, so white letters on it pass 4.5:1 at key size. There is one gradient, on the cover, to keep the type on solid lapis. No gradient text.

Type. Unbounded (500 to 800) for the wordmark, tile letters, keys, big numbers and headings, tightly tracked at large sizes. Atkinson Hyperlegible Next (400, 500, 700) for everything you read, at line height 1.6. Radii: 4px for tiles and tags, 6px for controls, 12px for sheets.

Game feel. Every interaction answers back. Keys are raised and sink 3px while pressed, also when you type on a physical keyboard. A tile pops as a letter lands in it. Each revealed tile turns about its upright axis, lifts 9px with a shadow at the edge on moment, takes its colour there and lands with a small press. A refused word gets a gentle, decaying shake. A win lifts the row in a wave. Sound is optional, as above.

The one orchestrated moment: the win. After the winning row lifts, the result panel lays a 9 by 9 mosaic medallion: 81 tesserae fall in a diagonal wave, squash slightly on landing, settle at their own small hand laid tilt, and the whole medallion is pressed once into its bed. The numeral is the guess count in black stone inside a lapis border with terracotta corners. About 1.4 seconds, then the words and buttons follow. No confetti.

Other motion, one job each. A staggered entrance once per visit (the cover's wordmark tiles drop in, or the board rows rise); animation-timeline: view() scroll reveals inside @supports, with an IntersectionObserver fallback at threshold 0.2; a clip-path reveal on the How to play photograph; a sticky photo column on How to play; document.startViewTransition for screen changes (feature detected, skipped when the page is hidden); hovers of 150 to 200 ms with no bounce. All of it is off under reduced motion.

Photography. One shoot: late morning sun raking low from the left, warm neutral white balance, macro lens.

NameSizeWhere
cover1600 by 893, widescreenGallery card and share image (og:image), and the How to play photo column
texture800 by 800, squareBehind the Today cover at 30% in a luminosity blend; decorative, so its alt is empty

Both are referenced as /template-media/word-guess/<name>.webp; media.json holds the prompts. The texture loads with fetchpriority="high" and the cover lazily. Each sits on a lapis fallback colour, so the layout holds while it loads.

Craft notes

  • Light and dark themes plus high contrast in both. Every colour is a token in styles.css.
  • No alert, confirm or prompt: confirmations happen in the page. Focus is always visible, every input is labelled, motion settles and respects reduced motion, and the layout works from 360 px up. On phones the bottom tab bar holds Today, Practice, Stats, Create and Settings; on desktop they sit in the top bar.
  • The on screen keyboard never takes focus when tapped, so a physical Enter always submits the guess, while a keyboard user who tabs to a button still gets that button's own Enter.

Left out on purpose

  • Accounts, sync and a real leaderboard (see above). The demo board is sample data.
  • Push or email reminders for the next puzzle.
  • Other word lengths, other languages, and a themed word list per week.
  • An offensive word filter for friend puzzles beyond the built in list check; add one before a public launch.
  • Native app packaging. To ship to stores later, wrap the built site with Capacitor and add icons and a privacy policy.