Title: When Worlds Collide: CSS-Only Collision Detection and the Magic of Modern CSS

A friend sends Lee Meyer a link to a mesmerizing CodePen demo by Manuel Schaller — a fully automated, endlessly looping recreation of the classic arcade game Pong, made entirely in pure CSS. The paddles move on their own, the ball bounces forever, and the whole scene feels like an old arcade cabinet stuck in attract mode, waiting for a coin. For Lee, it triggers a wave of nostalgia and a strange memory: a mysterious token given to him by a spooky shopkeeper at a haunted CSS Tricks carnival booth. He notices an unfamiliar slot on his laptop, inserts the token, and suddenly the CodePen reloads — this time with a paddle he can control and a life counter that tracks his success. The game is on.

This whimsical intro sets the stage for something quite real: a deep dive into how modern CSS now allows for collision detection and interactivity — things that once seemed impossible without JavaScript.

CSS Collision Detection: Then vs. Now

Building a game like Pong in CSS isn’t new — back in 2013, Alex Walker created a CSS-only Pong using a clever mix of checkbox hacks, sibling selectors, and hover states. He humorously described it as “a glittering city of hacks built on the banks of the ol’ Hack River. On the Planet Hack.”

Fast forward to today, and we’re in a new era. The latest CodePen demo uses style queries, animatable custom properties, and animation timelines to create a more maintainable and expressive version of CSS Pong. The result? A cleaner, more powerful implementation that feels less like a hack and more like a legitimate use of CSS’s growing capabilities.

Collision Detection with Style Queries

In 2025, making interactive CSS animations with elements bouncing off each other is surprisingly achievable. While no one is seriously suggesting CSS should replace JavaScript for game development, the increasing power of CSS makes it tempting to try.

Unlike the 2013 version, which relied heavily on :hover (not ideal for touch devices), this new version uses range inputs and view progress timelines — making it playable on mobile and desktop alike.

The real magic happens in the collision detection, which is powered by style queries. When the ball reaches the player’s side, CSS checks whether the paddle is in the right position. If it is, the background flashes green. If not, it turns red and the life counter decreases.

Here’s a simplified explanation of how it works:

– The ball follows a fixed path.
– When it hits the left side of the screen, CSS checks if the paddle is aligned with the ball.
– If the paddle is in range, the animation that reduces the life counter stays paused.
– If not, the animation runs briefly to decrease the life count.

Since style queries don’t support greater-than or less-than comparisons, the implementation uses the min() function as a workaround. It’s not elegant, but it works.

Responding to Collisions

To simulate the consequences of a missed ball, CSS uses keyframe animations and custom properties. The animation that decrements lives is paused by default. When a miss is detected, it briefly unpauses to step forward — from 3 lives to 2, and so on. Once lives reach zero, the game field disappears and a “Game Over” screen appears.

This technique shows how style queries can indirectly control even non-animatable properties by leveraging keyframe animations and custom properties. It’s a clever workaround for CSS’s limitations — and a reminder that with great power comes great complexity.

Timing and Sync Issues

One of the trickiest parts of the implementation was syncing the ball animation with the life counter. Lee discovered that even though both animations are deterministic, they didn’t always behave predictably. Sometimes, a missed ball wouldn’t decrement the life counter at all — or worse, it would decrement more than once.

After some trial and error, he found that setting the ball animation to 8 seconds and the life counter to 80 milliseconds fixed the issue. Why? He has a few theories:

– CSS animations can suffer from timer drift — slight inconsistencies in timing that accumulate over time.
– Using translate3d() might improve performance and precision by offloading animation to the GPU.
– Style queries may have unpredictable latency, leading to race conditions.
– Or maybe — just maybe — it’s all part of the spooky shopkeeper’s curse.

A Word of Caution

The implementation pushes CSS to its limits, and while it works, it’s not without quirks. The spooky shopkeeper’s warning — that developers have gone mad trying to master these features — might not be far off. But it’s also a testament to how far CSS has come and how much further it might go.

Outro

You finish reading, skeptical of Lee’s explanation for the animation bug fix. Surely, you could do better. You open the CodePen

Similar Posts