I built a small Halloween game for the readers of my newsletter, Stef's Dev Notes.
Each card is a moment from building products, taken from something I've written. You call it a trick or a treat, the card flips to show why, and every right call keeps a cookie on your plate. Get one wrong and a ghost takes a bite.
👉 Play it here: Trick or Treat: Builder Edition
It takes about two minutes and I hope you'll get something out of it - either a learning or a small moment of joy.
Below are the decisions behind it.
No framework
The game is one page with ten cards. There are no accounts, and no data comes from a server.
So I didn't reach for a framework. It's plain HTML, CSS and JavaScript in a single index.html, plus a small vercel.json. There's nothing to install and nothing to build.
It's the same test I use for AI features: if the simplest version does the job, I don't add more.
The card flip
Each card has a front and a back stacked in the same grid cell. Flipping it is one class on the parent:
`css
.flip { perspective: 1600px; }
.flip-inner {
display: grid;
transform-style: preserve-3d;
transition: transform .75s cubic-bezier(.3, .7, .2, 1);
}
.flip.flipped .flip-inner { transform: rotateY(180deg); }
.face { grid-area: 1 / 1; backface-visibility: hidden; }
.face.back { transform: rotateY(180deg); }`
One bug taught me something here. One of the cards in the game had a drop-in animation on the same element as the 3D transform. While it ran, the browser flattened the card, and the back of the front face showed through, mirrored. Moving the entrance animation to an outer wrapper fixed it.
Dealing balanced hands
The deck has ten cards, five tricks and five treats, and each game deals five.
A random five could be all tricks, which isn't much of a game. So the deck splits into two balanced hands of two or three of each. Your second game deals the five cards you haven't seen. After both, it reshuffles.
function dealHand() {
if (unseen.length < HAND) {
const tricks = shuffle(ROUNDS.filter(r => r.answer === "trick"));
const treats = shuffle(ROUNDS.filter(r => r.answer === "treat"));
const k = Math.random() < .5 ? 2 : 3;
const first = shuffle([...tricks.slice(0, k), ...treats.slice(0, HAND - k)]);
const second = shuffle([...tricks.slice(k), ...treats.slice(HAND - k)]);
unseen = [...first, ...second];
}
return unseen.splice(0, HAND);
}
Accessibility
A card that flips is a visual idea, so I made sure it works without seeing it:
- The face you can't see is inert and aria-hidden, so keyboard and screen reader users never land on hidden text.
- A polite live region announces each new card, the verdict with its explanation, and the final score.
- Focus moves to the new card after "Draw the next card", and to the result at the end.
- The cookie plate has a text label, like "1 treat kept, 1 trick caught, 1 bitten, 2 to go".
- The arrow keys pick Trick and Treat.
- With reduced motion turned on, the card turns over instantly and nothing drifts.
- One screen on desktop
On desktop, the whole game fits in the window without scrolling. The layout splits into two halves, and text, cards and spacing scale with the window height using clamp() and vh. On smaller screens it falls back to a normal stacked page that scrolls.
The full story
I built it with Claude, then kept tweaking it before launch. The idea came from Karo Zieminski's article on building a Halloween game for your readers.
I wrote about why I built it and what changed along the way on my newsletter: I built a Halloween game for you.
If you play, I'd love to know your score. 🎃
