A good friend DMs Lee Meyer a CodePen by Manuel Schaller containing a pure CSS simulation of one of many world’s earliest arcade video games, Pong, with each paddles taking part robotically, in an limitless loop. The demo reminds Lee of an arcade machine in appeal to mode awaiting a coin, and the long-lasting imagery awakens muscle reminiscence from his misspent childhood, inflicting him to look his pocket by which he finds the token a spooky shopkeeper gave him final 12 months on the CSS methods stall within the haunted carnival. The token gleams like a power-up within the mild of his laptop computer, which has a slot he by no means observed. He feeds the token into the slot, and the CodePen reloads itself. A vertical vary enter and a life counter seem, permitting him to manage the left paddle and play the sport in Chrome utilizing a cocktail of contemporary and experimental CSS options to implement collision detection in CSS animations. He recollects the spooky shopkeeper’s warning that enjoying with these options has pushed some builders to insanity, however the shopkeeper’s voice in Lee’s head whispers: “Too late, we’re already enjoying.”
CSS collision detection: Previous and current
So, perhaps the expertise of utilizing trendy CSS so as to add collision detection and interactivity to an animation wasn’t as very similar to a screenplay sponsored by CSS as I depicted within the intro above — however it did really feel like magic in comparison with what Alex Walker needed to undergo in 2013 to attain an identical impact. Hilariously, he describes his implementation as “a glittering metropolis of hacks constructed on the banks of the ol’ Hack River. On the Planet Hack.“ Alex’s model of CSS Pong cleverly combines checkbox hacks, sibling selectors, and :hover
, whereas the CodePen under makes use of model queries to detect collisions. I really feel it’s a pleasant illustration of how far CSS has come, and a testomony to elevated energy and expressiveness of CSS greater than a decade later. It reveals how a lot energy we get when combining new CSS options — on this case, that features model queries, animatable customized properties, and animation timelines. The longer term CSS options of inline conditionals and customized features may have the ability to simplify this code extra.
Collision detection with model queries
Interactive CSS animations with parts ricocheting off one another appears extra believable in 2025 and the code is considerably wise. Whereas it’s pointless to implement Pong in CSS, and the CSS Working Group most likely hasn’t been considering easy methods to make that specific area of interest activity simpler, the rising flexibility and energy of CSS reinforce my suspicion that sooner or later it will likely be a way of life alternative whether or not to attain any given impact with scripting or CSS.
The demo is an identical variety of traces of CSS to Alex’s 2013 implementation, however it didn’t really feel very similar to a hack. It’s a demo of contemporary CSS options working collectively in the way in which I anticipated after studying the instruction booklet. Generally when studying introductory articles in regards to the new options we’re getting in CSS, it’s arduous to understand how game-changing they’re until you see a number of options working collectively. As usually occurs when pushing the boundaries of a know-how, we’re going to bump up in opposition to the present limitations of favor queries and animations. However it’s all in good enjoyable, and we’ll study these CSS options in additional element than if we had not tried this loopy experiment.
It does appear to work, and my 12-year-old and 7-year-old have each playtested it on my cellphone and laptop computer, so it will get the “works on Lee’s gadgets” seal of high quality. Additionally, since Chrome now helps controlling animations utilizing vary inputs, we will make our recreation playable on cell, in contrast to the 2013 model, which relied on :hover
. Temani Afif gives a nice clarification of how and why view progress timelines can be utilized to model something based mostly on the worth of a spread enter.
Utilizing model queries to detect if the paddle hit the ball
The ball follows a hard and fast path, and whether or not the participant’s paddle intersects with the ball when it reaches our facet is the one enter we’ve got into whether or not it continues its predetermined bouncy loop or the display screen flashes pink because the life counter goes down until we see the “Recreation Over” display screen with the choice to play once more.
This kind of interactivity is what recreation designers name a fast time occasion. It’s nonetheless a recreation for positive, however 5 months in the past, once I was younger and naive, I mused in my article on animation timelines that the animation timeline characteristic might open the door for superior video games and interactive experiences in CSS. I wrote {that a} online game is only a “hyper-interactive animation.” Certainly, the above experiment reveals that the brand new options in CSS enable us to reply to consumer enter in subtle methods, however the demo additionally clarifies the distinction between the type of interactivity we will count on from the present incarnation of CSS versus scripting. The above experiment is extra like if Pong have been a recreation contained in the old-school arcade recreation Dragon’s Lair, which was one big fast time occasion. It solely works as a result of there are restricted attainable outcomes, however they’re definitely much less restricted than what we used to have the ability to obtain in CSS.
Since we all know collision detection with the paddle is the one alternative for the consumer to have a say in what occurs subsequent, let’s give attention to that implementation. It’ll require extra psychological gymnastics than I would love, since container model queries solely enable for name-value pairs with the identical syntax as characteristic queries, that means we will’t use “higher than” or “lower than” operators when evaluating numeric values like we do with container dimension queries which observe the identical syntax as @media
dimension queries.
The workaround under permits us to create model queries based mostly on the ball place being in or out of the vary of the paddle. If the ball hits our facet, then by default, the play area will flash pink and briefly unpause the animation that decrements the life counter (extra on that later). But when the ball hits our facet and is inside vary of the paddle, we depart the life-decrementing animation paused, and make the sector background inexperienced whereas the ball hits the paddle. Since we don’t have “higher than” or “lower than” operators in model queries, we (ab)use the min()
perform. If the consequence equals the primary argument then that argument is lower than or equal to the second; in any other case it’s higher than the second argument. It’s logical however made me want for higher comparability operators in model queries. Nonetheless, I used to be impressed that model queries enable the collision detection to be pretty readable, if just a little extra verbose than I would love.
physique {
--int-ball-position-x: spherical(down, var(--ball-position-x));
--min-ball-position-y-and-top-of-paddle: min(var(--ball-position-y) + var(--ball-height), var(--ping-position));
--min-ball-position-y-and-bottom-of-paddle: min(var(--ball-position-y), var(--ping-position) + var(--paddle-height));
}
@container model(--int-ball-position-x: var(--ball-left-boundary)) {
.display screen {
--lives-decrement: working;
.area {
background: pink;
}
}
}
@container model(--min-ball-position-y-and-top-of-paddle: var(--ping-position)) and elegance(--min-ball-position-y-and-bottom-of-paddle: var(--ball-position-y)) and elegance(--int-ball-position-x: var(--ball-left-boundary)) {
.display screen {
--lives-decrement: paused;
.area {
background: inexperienced;
}
}
}
Responding to collisions
Now that we will model our enjoying area based mostly on whether or not the paddle hits the ball, we wish to decrement the life counter if our paddle misses the ball, and show “Recreation Over” after we run out of lives. One solution to obtain negative effects in CSS is by pausing and unpausing keyframe animations that run forwards. Nowadays, we will model issues based mostly on customized properties, which we will set in animations. Utilizing this truth, we will take the facility of paused animations to a different stage.
physique {
animation: ball 8s infinite linear, lives 80ms forwards steps(4) var(--lives-decrement);
--lives-decrement: paused;
}
.lives::after {
content material: var(--lives);
}
@keyframes lives {
0% {
--lives: "3";
}
25% {
--lives: "2";
}
75% {
--lives: "1";
}
100% {
--lives: "0";
}
}
@container model(--int-ball-position-x: var(--ball-left-boundary)) {
.display screen {
--lives-decrement: working;
.area {
background: pink;
}
}
}
@container model(--min-ball-position-y-and-top-of-paddle: var(--ping-position)) and elegance(--min-ball-position-y-and-bottom-of-paddle: var(--ball-position-y)) and elegance(--int-ball-position-x: 8) {
.display screen {
--lives-decrement: paused;
.area {
background: inexperienced;
}
}
}
@container model(--lives: '0') {
.area {
show: none;
}
.game-over {
show: flex;
}
}
So when the ball hits the wall and isn’t in vary of the paddle, the lives-decrementing animation is unpaused lengthy sufficient to let it full one step. As soon as it reaches zero we cover the play area and show the “Recreation Over” display screen. What’s fascinating about this a part of the experiment is that it reveals that, utilizing model queries, all properties develop into not directly attainable to manage through animations, even when working with non-animatable properties. And this is applicable to properties that management whether or not different animations play. This article touches on why play state intentionally isn’t animatable and could possibly be harmful to animate, however we all know what we’re doing, proper?
Full disclosure: The play state method did result in hidden complexity within the alternative of period of the animations. I knew that if I selected too lengthy a period for the life-decrementing counter, it won’t have time to proceed to the following step whereas the ball was hitting the wall, but when I selected too quick a period, lacking the ball as soon as may trigger the participant to lose a couple of life.
I made educated guesses of appropriate durations for the ball bouncing and life decrementing, and I anticipated that when working with fixed-duration predictable animations, the life counter would both all the time work or all the time fail. I didn’t count on that my first try on the implementation intermittently didn’t decrement the life counter on the similar level within the animation loop. Setting the durations of each these associated animations to multiples of eight appears to repair the issue, however why would predetermined animations exhibit unpredictable habits?
Forefeit the sport earlier than any individual else takes you out of the body
I’ve theories as to why the unpredictability of the collision detection gave the impression to be fastened by setting the ball animation to eight seconds and the lives animation to 80 milliseconds. Once more, pushing CSS to its limits forces us to suppose deeper about the way it’s working.
- CSS seems to endure from timer drift, that means when you set a keyframes animation to final for one second, it’s going to generally take just below or over one second. When there’s a completely different charge of change between the ball-bouncing and life-losing, it could make sense that the potential discrepancy between the 2 could be pronounced and result in unpredictable collision detection. When the speed of change in each animations is identical, they’d endure about equally from timer drift, that means the frames nonetheless synchronize predictably. Or no less than I’m hoping the possibility they don’t turns into negligible.
- Alex’s 2013 model of Pong makes use of
translate3d()
to maneuver the ball regardless that it solely strikes in 2D. Alex recommends this at any time when attainable “for environment friendly animation rendering, offloading processing to the GPU for smoother visible results.” Doing this will likely have been another repair if it results in extra exact animation timing. There are tradeoffs so I wasn’t prepared to go down that rabbit gap of attempting to tune the animation efficiency on this article — however it could possibly be an fascinating focus for future analysis into CSS collision detection. - Perhaps model queries take a various period of time to kick in, resulting in some type of a race situation. It’s attainable that making the ball-bouncing animation slower made this downside much less seemingly.
- Perhaps the bug stays lurking within the shadows someplace. What did I count on from a hack I achieved utilizing a magic token from a spooky shopkeeper? Haven’t I seen any eighties film ever?
Outro
You end studying the article, and really feel positive that the creator’s rationale for his supposed repair for the bug is hogwash. Clearly, Lee has been pushed insane by the attract of overpowering new CSS options, whereas you respect the facility of CSS, however you additionally respect its limitations. You sit right down to spend a couple of minutes with the collision detection CodePen to show it’s nonetheless damaged, however then discover different flaws within the collision detection, and also you start work on a fork that might be superior. Hey, talking of timer drift, how is it out of the blue 1 a.m.? Solely a loopy particular person would keep up that late enjoying with CSS once they need to work the following day. “Insanity,” repeats the spooky shopkeeper inside your head, and his laughter echoes someplace within the evening.
Roll the credit
This looping Pong CSS animation by Manuel Schaller gave me an incredible foundation for including the collision detection. His twitching paddle animations assist give the phantasm of enjoying in opposition to a pc opponent, so forking his CodePen let me give attention to implementing the collision detection slightly than reinventing Pong.
This creator is grateful to the junior testing group, comprised of his seven-year-old and twelve-year-old, who declared the CSS Pong implementation “fairly cool.” Additionally they advised the inexperienced and pink flashes to sign collisions and misses.
The intro and outro for this text have been sponsored by the spooky shopkeeper who sells harmful CSS methods. He additionally sells frozen yoghurt, which he calls froghurt.