Build an HTML puzzle
A self-contained .html file the app can launch, drop a token into, and watch for a completion signal. That's the whole contract.
Quick start
Three steps.
- Create a self-contained
.htmlfile with your puzzle. - Upload it as an unlock condition in the clue editor.
- That's it — the app handles the rest.
The contract
Your HTML file must do two things.
1 Read the token from the URL on page load.
The app passes a secret token via the URL fragment. Read it like this:
const token = new URLSearchParams(
window.location.hash.substring(1)
).get('token');
2 Signal completion when the player solves it.
Send the token back to the app. Use both methods to support all platforms:
function completePuzzle() {
if (!token) return;
// Mobile (Android/iOS)
if (typeof PuzzleComplete !== 'undefined') {
PuzzleComplete.postMessage(token);
}
// Web
try {
if (window.parent !== window)
window.parent.postMessage(token, '*');
} catch(e) {}
try {
if (window.top !== window)
window.top.postMessage(token, '*');
} catch(e) {}
}
Call completePuzzle() when the player wins.
The rules
Four constraints.
One file — inline all CSS and JavaScript. External CDN links (e.g. Google Fonts, libraries) are OK.
No relative paths — the file is served from cloud storage, so relative paths won't resolve.
Don't hardcode the token — it's provided at runtime via the URL fragment.
Mobile-friendly — include
<meta name="viewport" content="width=device-width, initial-scale=1.0">Cross-browser notes
Test on iPhone, not just Android.
Android renders puzzles in Chromium; iOS uses WebKit. A puzzle that looks fine on Android Chrome (or desktop Chrome) can still misbehave on an iPhone. The usual suspects:
- Inputs auto-zoom on focus when their
font-sizeis below16px. Pininput,textareaandselectto at least16pxif you don't want the page to jump. 100vhjumps when the URL bar collapses and re-appears. Use100dvhwith a100vhfallback (declare both, in that order) for any full-viewport element.aspect-ratio+height: 100%inside a flex container collapses the width to ~0 on iOS Safari. Let an<img>'s natural ratio drive the box instead (height: 100%; width: autoon the img, noaspect-ratioon the wrapper), or setalign-self: stretchon the child.- Gray tap flash on every tap — set
-webkit-tap-highlight-color: transparenton the body if it bothers you. - Sluggish taps — add
touch-action: manipulationon interactive elements to skip the legacy double-tap-zoom delay.
If you can, test on a real iPhone before publishing. The iOS Simulator (Xcode) is a close second.
Reference
Minimal working example.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>My Puzzle</title>
</head>
<body>
<h1>Type the magic word</h1>
<input type="text" id="answer"
placeholder="Enter answer...">
<script>
const token = new URLSearchParams(
window.location.hash.substring(1)
).get('token');
function completePuzzle() {
if (!token) return;
if (typeof PuzzleComplete !== 'undefined')
PuzzleComplete.postMessage(token);
try { if (window.parent !== window)
window.parent.postMessage(token, '*');
} catch(e) {}
try { if (window.top !== window)
window.top.postMessage(token, '*');
} catch(e) {}
}
document.getElementById('answer')
.addEventListener('input', (e) => {
if (e.target.value.trim()
.toLowerCase() === 'hello') {
e.target.disabled = true;
completePuzzle();
}
});
</script>
</body>
</html>
Sparks
Some directions.
You can build anything — the only limit is what HTML, CSS and JS can do:
Word puzzles
Code-breaking
Jigsaw puzzles
Quizzes
Logic grids
Cipher decoders
Hidden objects
Memory games
3D mazes
Sound-based