Previously, I have made a calculator and tic-tac-toe with a “random” computer player, both entirely in CSS. I also once made a game of flappy bird in D3 however not in any way that warranted a blog post. Recently a colleague brought it up and had misremembered it as “flappy bird in CSS”. The thought was amusing. I knew it would be technically possible with the use of animated radio buttons for every single possible state, but that didnt feel interesting. Over the next few weeks I never actively persued the idea but kept getting little thoughts about how different bits could be achieved. Until finally I was forced to sit down and make it. In this cry for help blog post I explain how I made it.
Rules
The only thing I wrote was HTML and CSS. No HAML or SCSS or any other pre-processors. The no JavaScript is enforced by testing the app with JavaScript disabled by the browser. You can view my full codebase, including other creations here.
How did I make it?
Click to jump
The fundamental aspect of the game, click a button and bird jumps up, before falling to the ground ( or in my case off the screen ).
Motion is simple, I played around and found an animation setting that looked close enough. I’m not going to explain the cubic-bezier here, just know that by setting up the below, we can animate the CSS variable --bird-delta-y to go up and then down in a falling manner. And by adding this value to the birds positon, the bird is animated.
@property --bird-delta-y {
syntax: "<length>";
initial-value: 0px;
inherits: true;
}
:root {
animation-name: jumpAndFall;
}
@keyframes jumpAndFall {
0% {
--bird-delta-y: 0;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
25% {
--bird-delta-y: calc(-1 * var(--jump-height));
animation-timing-function: cubic-bezier(0.68, 0, 1, 0.26);
}
100% {
--bird-delta-y: var(--fall-distance)
}
}
.bird {
postion: absolute;
top: calc(30px + var(--furthest-click-dist))
}
Next we need to reset the jump and start it from a new location when the player clicks their mouse. Its not possible to just read the current value of --bird-delta-y and base new calculations off it, this is because CSS works in a declarative manner not a imperative one. Its also not possible to do an event listener in CSS, but I can use radio input buttons. CSS can detect a checked radio button and can thus apply styles or modify variables. And the nature of radio buttons is such that if another one is clicked the first one becomes unchecked. So the value of --active-number below will always be the value of the most recently clicked radio button.
:root:has(input[id^="1"]:checked) {
--active-number: 1;
}
/* And so on */
So next we stack a bunch of radio buttons on top of each other (I actually used label elements). And animate them along with the bird. By restricting the user to only be able to click the button in a specific positon, we can make calculations based on the radio button selected on where the bird needs to jump from, this is best understood from the animation below.
The key thing to remember that in the actual game the shaded regions are completely grey. The css logic looks like..
css
--bird-frame-top: calc(
var(--bird-frame-base-top) + var(--bird-delta-y) + var(--active-number) * var(--click-box-height)
);
--click-box-height is the height of the label, this does mean that there is a precision limit based simply on how large the labels are. --bird-frame-base-top is just a base value for the position of the bird frame (bird and labels). --active-number is determined by the most recent label clicked, each value is just an integer indicating its position. --bird-delta-y is the animated variable from earlier.
The code above changes the nature of the animation, but we still need to reset the animation on each click, this is done by simply having two sets of inputs, which swap in and out on every click . Each set has a full collection for setting --active-number, however they set a different identical animation. The changing of the animation, resets it.
:root:has(input[id$="fall1"]:checked) {
animation-name: jumpAndFall2;
}
:root:has(input[id$="fall2"]:checked) {
animation-name: jumpAndFall;
}
@keyframes jumpAndFall {
/* As above */
}
@keyframes jumpAndFall {
/* Duplicate */
}
/* Ensure that the jump-holders (divs contains labels) swap in and out on every click */
div:has(input[id$="fall1"]:checked) ~ * #jump-label-holder-2,
#jump-label-holder-1
{
display: flex;
}
div:has(input[id$="fall1"]:checked) ~ * #jump-label-holder-1,
#jump-label-holder-2
{
display: none;
}
All the radio buttons are have the same name which means that only one can be selected at any one time. So when one from #jump-label-holder-2 is selcted it deslectes the one from #jump-label-holder-1.
If a user clicks down on a label, it is possible for that label to move out of the way and another label takes its place, then no selection is detected. To deal with this we can increase the height of the label when the user presses down by making use of the :active label. We can also use :active to animate the wings by simply swapping out the image.
Pipes and “Randomness”
//score ### Collision Detection and game end
variables mentioned above ar in the root go below end click too long caveat // transform messes with :hover, direct position does not // game over screen bye bye aniamtion ###
FAQ
After my recent rise to global stardom
The game looks kind of ugly, have you considered adding pretty styling?
I’ve never heard of CSS being used for styling, but anything is possible I guess.
I have square birds for anthropoligcal accuracy and not because my intial work
Wrap up
So what does this tell us? I am smarter than Gemini (at the time of writing this). Please consider // show screenshot of gemini attempt // console.log in CSS //dimenstionality // need to explain has? //