A small website of games for 2–3 year olds, made for tablets and made to be hosted at home. It is plain static files: no accounts, no ads, no tracking, and nothing is fetched from the internet once it is running on your network.
| Game | What you do |
|---|---|
| Bubbles | Tap or swipe the floating bubbles to pop them. Every so often a star bubble floats up and throws confetti. |
| Shapes | Drag each shape into the hole it fits. Fill all three for a celebration and a new set. |
| Music | A rainbow xylophone. Tap a bar, slide across them, or use both hands. |
Built for small hands:
- Nothing to read, and no scores, timers or ways to lose.
- Big targets, and every finger on the screen counts.
- The home button (top right, in a game) needs a press-and-hold, so a stray palm doesn't end the game.
- Zooming, text selection and long-press menus are switched off.
You need a TrueNAS version with Docker-based apps (24.10 "Electric Eel" or
newer). Both options use Apps → Discover Apps → ⋮ next to Custom App →
Install via YAML. Name the app toddler-games, paste the YAML, and save.
Then open http://<your-nas-ip>:8080 on the tablet.
If port 8080 is already used on your NAS, change the left number in
"8080:8080".
Every push to master builds the site into a container image and publishes it
to ghcr.io/schentrup-software/toddler-games (see
.github/workflows/publish.yml).
services:
toddler-games:
image: ghcr.io/schentrup-software/toddler-games:latest
pull_policy: always
ports:
- "8080:8080"
restart: unless-stoppedThe first time the workflow runs, GitHub may create the package as private. If
TrueNAS can't pull it, open the package on GitHub and set its visibility to
public, or add a ghcr.io login in the TrueNAS Apps settings.
To update, push your change, wait for the workflow to finish, then stop and start the app so it pulls the new image.
Copy the site folder and nginx.conf from this repo into a dataset, for
example /mnt/tank/apps/toddler-games/. The container runs as TrueNAS's
built-in apps user (ID 568), so that user needs read access to the dataset.
services:
toddler-games:
image: nginxinc/nginx-unprivileged:stable-alpine
user: "568:568"
ports:
- "8080:8080"
volumes:
- /mnt/tank/apps/toddler-games/site:/usr/share/nginx/html:ro
- /mnt/tank/apps/toddler-games/nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stoppedTo update, copy the new files over the old ones. No restart is needed.
- iPad: open the site in Safari, then Share → Add to Home Screen. It then opens full screen like an app. Guided Access (Settings → Accessibility) keeps a child from leaving it.
- Android: tap the full-screen button at the top right of the home screen. App pinning (Settings → Security) keeps a child from leaving it.
docker compose up --build # http://localhost:8080Or without Docker, with any static file server:
python -m http.server 8080 --directory siteOpening index.html straight from disk does not work; browsers only load
JavaScript modules over HTTP.
There is no build step. Each game is a folder with two files.
-
Create
site/games/<id>/game.js:export function start(stage) { // Build the game inside `stage`, which fills the screen. return () => { // Stop timers and remove what you added. }; }
-
Create
site/games/<id>/game.cssfor its styles. -
Add an entry to site/js/registry.js. That puts a card on the home screen and makes
#/<id>open the game.
Things to reuse:
- site/css/base.css holds the pastel palette. Add a
c-<name>class (coral,peach,butter,mint,aqua,sky,lilac,pink) to an element and style it withvar(--c)and the darkervar(--cd). - site/js/audio.js has the sounds (
mallet,pop,boop,chime,fanfare). They are synthesised, so there are no audio files. - site/js/fx.js has
burstandconfetti.
site/ the website; this folder is all that gets served
index.html home screen and the shell that games run in
css/base.css palette and shared styles
js/ router, game list, sounds, effects
games/<id>/ one folder per game
Dockerfile nginx serving ./site as a non-root user on port 8080
nginx.conf nginx site config
compose.yaml build and run with Docker Compose
MIT. See LICENSE.