My site’s moved CMS and servers. If you spot a problem, let me know.

Stuff & Nonsense product and website design
Can you catch ’em all?

Can you catch ’em all?

I’d been tinkering with animations last week and wondered what else I could do with my Magnificent 7 characters. I love surprising people with hidden Easter Eggs, so I decided to use them in a little hidden game.

Press the mysterious-looking question mark under any of the animated graphic banners, and a collection of wanted posters pops up in a dialog. One of them is catchable. Press the button to capture him and collect the reward. Only one of the characters is catchable at a time, and there’s a different character to capture on each page.

Wanted posters
My complete series of wanted posters

I started building the game by making a black-and-white wanted poster version of each character’s face. These are SVGs and optimised; each weighs around 8kb.

Wanted poster version of each character’s face

Then I made a torn paper border, which weighs less than 1kb, so all the graphics combined weigh more than 50kb. I really love SVG.

Six SVG graphics, weighing in at just 50kb

Once I had all the outlaws staring back at me, I needed to present them in a way that felt like part of the site rather than a separate element. A <dialog> turned out to be perfect for that. It contains a header and a placeholder for the posters:

<dialog class="game-dialog">
 <button id="close-dialog">×</button>
 <header class="game-header">
 […]
 </header>
 <div class="game-content">
 <div id="posters-container">
 <!-- posters -->
 </div>
 </div>
</dialog>

A script then creates each of the posters:

<div class="game-poster">
<p class="status">[…]</p>
 <div class="game-svg">
  <svg>[…]</svg>
 </div>
<p class="game-reward">[…]</p>
</div>

It adds a data- attribute for each poster, plus another class attribute if the character has been caught:

<div class="game-poster game-captured" data-id="1">
 […]
</div>

I turned my attention to writing the CSS, starting with the dialog element. It fills 80% of the viewport width and 90% of its height and is centered horizontally and vertically:

.game-dialog {
height: 90vh;
max-width: 1200px;
position: fixed;
top: 50%;
transform: translate(-50%, -50%);
width: 80vw; }

When the dialog is open, the ::backdrop is slightly transparent to allow a hint of the page behind to peek through:

.game-dialog::backdrop {
background-color: #161d1a;
opacity: .75; }

With the dialog and its ::backdrop working, I could focus on how the posters should look and behave. The posters needed to look like they’ve been tacked to a wall, while still adapting to different screen sizes.

Posters grid

On small screens, the posters are arranged in a horizontally scrolling panel.

Small screen horizontal scrolling

So I placed them into a grid container with six columns:

#posters-container {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 1rem;
max-width: 100%;
overflow-x: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch; }
Large screen grid layout

For larger screens, I reduced the grid to three columns:

@media screen and (min-width: 64em) {
#posters-container {
grid-template-columns: repeat(3, 1fr);
overflow-x: visible;
max-width: none; }
}

Wanted posters

The posters themselves have a torn-paper border applied with border-image, one of the least-used CSS properties:

.game-poster {
border-image-slice: 40 fill;
border-image-width: 40px;
border-image-repeat: stretch;
border-image-source: url("[…]");
border-style: solid; }
Large screen grid layout with rotations

Finally, to break the rigidity of the grid, I rotated some of the posters:

.game-poster {
rotate: 0deg; }
.game-poster:nth-of-type(1),
.game-poster:nth-of-type(5) {
rotate: -2deg; }
.game-poster:nth-of-type(3) {
rotate: 2deg; }

Now that the posters looked the part, it was time to add interactivity. A few subtle animations can turn what’s essentially a static grid into a design that feels tactile.

Animations and interactions

Elements that respond to someone’s actions can help elevate what would otherwise be a static design. So first I reset those rotations on :hover:

.game-poster {
rotate: 0deg;
scale: 1;
transition: all var(--animate-duration-faster) ease-in; }
.game-poster:hover {
rotate: 0deg; }
Large screen grid layout with interactions

Then—using one of my favourite :has techniques—I reduced the size of the posters except for the one being hovered over:

#posters-container:has(.game-poster:hover) .game-poster:not(:hover) {
scale: .95; }

Finally, to make a poster shake when someone presses the capture button, I defined a shaking animation and applied it to a poster when it contains an :active capture button:

@keyframes poster-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-1.5deg); }
75% { transform: rotate(1.5deg); }
}
.game-poster:has(.game-capture:active) {
animation: poster-shake .2s ease-in-out infinite; }

With the animations in place, the next step was to show which characters had already been caught.

Captured stamp

Each poster contains a class attribute which indicates whether a character has been captured. It also includes a status paragraph with values including “game-available”, “game-captured”, and “game-wanted”:

<div class="game-poster captured">
<p class="status game-captured">[…]</p>
[…]
</div>

When a character is available to capture or is just wanted, this stamp is included at the top of a poster.

Wanted poster with captured stamp

But once they’ve been caught, this status turns into a red rubber stamp across the poster. For this, I styled the status stamp, positioned, then rotated it on the poster:

.game-poster.captured {
position: relative; }
.status.game-captured {
background-color: rgba(230,250,240,.75);
border: 5px solid var(--game-accent);
border-radius: 5px;
color: rgba(90,10,25,.75);
left: 10%;
padding: 1rem;
position: absolute;
rotate: -30deg; }

Eyes blinking

I’ve written about ambient animations a fair bit recently (1, 2) and wanted to add a few subtle animations to my posters to indicate which characters are available and those who have been captured. I decided to do this by opening and closing their eyes.

Closed eyelids paths

First, I added extra paths for each character’s closed eyelids into their SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1800 1800">
<g>[…]</g>
<path class="game-eyelids" d="[…]"/>
</svg>

Each poster has a data-id and includes its character’s status:

<div class="game-poster" data-id="1">
<p class="status game-available">Capture this outlaw</p>
<svg>[…]</svg>
</div>

When a character is available to be caught, I change the eyelid opacity to 0 and apply a blinking animation:

.game-poster:has(.status.game-available) .game-eyelids {
animation: eyelids 4s infinite;
opacity: 0; }
@keyframes eyelids {
0%, 92% { opacity: 0; }
93%, 94% { opacity: 1; }
95%, 97% { opacity: 0.1; }
98%, 100% { opacity: 0; }
}
Eyes variations

But when a character has already been captured, his eyes stay closed:

<div class="game-poster" data-id="1">
<p class="status game-captured">Captured</p>
<svg>[…]</svg>
</div>
.game-poster:has(.status.game-captured) .game-eyelids {
opacity: 1; }

Reduced motion

Not everyone experiences motion the same way. For some, even a small shake or flicker can feel distracting or disorienting. That’s why I always wrap animations inside a media query that checks for the user’s motion preferences. The prefers-reduced-motion feature lets me detect when someone’s system is set to limit motion, so I can adapt the design accordingly. I only apply the shaking animation when someone hasn’t asked for reduced motion:

@media (prefers-reduced-motion: no-preference) {
.game-poster:has(.game-capture:active) {
animation: poster-shake .2s ease-in-out infinite; }
@keyframes poster-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-1.5deg); }
75% { transform: rotate(1.5deg); }
}
}

This means the posters stay still for anyone who prefers less motion, while others still see the playful shake when they hit “Capture.”

Rewarding curiosity

I’ve always believed that the web should reward curiosity. Hiding this little Magnificent 7 game beneath my banners isn’t about gamification or engagement metrics, it’s a nod to the early web, when people built weird things for their own amusement.

Technically, this project reminded me why I still love SVG. The fact that I can fit my characters, a torn-paper frame, and all the surrounding interaction into less than 150kb still feels magical.


October 26, 2025 • Andy Clarke • designcustom

You might also like

Shop

Eleventy in a Box

A premium Eleventy starter kit for designers and developers who want to spend less time setting up the same project structure and more time designing distinctive websites.

Shop

Layout ❤︎

Free compound grid and modular grid layout generators, plus a set of HTML/CSS layout templates you can call on to make more interesting layouts, available to buy.