Pop(over) The Balloons | CSS-Tricks
popovertargetaction
attribute on it. See how challenging (in a good way) it is to “pop” the elements:
Styling balloons
Now we need to style the
and elements the same so that a player cannot tell which is which. Note that I said
and not
. That’s because
is the actual element we click to open and close the
container.
Most of this is pretty standard CSS work: setting backgrounds, padding, margin, sizing, borders, etc. But there are a couple of important, not necessarily intuitive, things to include.
- First, there’s setting the
list-style-type
property tonone
on the
element to get rid of the triangular marker that indicates whether the
- Safari doesn’t like that same approach. To remove the
marker here, we need to set a special vendor-prefixed pseudo-element,
summary::-webkit-details-marker
todisplay: none
. - It’d be good if the mouse cursor indicated that the balloons are clickable, so we can set
cursor: pointer
on the
elements as well.
- One last detail is setting the
user-select
property tonone
on the
s to prevent the balloons — which are simply emoji text — from being selected. This makes them more like objects on the page.
- And yes, it’s 2024 and we still need that prefixed
-webkit-user-select
property to account for Safari support. Thanks, Apple.
Putting all of that in code on a .balloon
class we’ll use for the and
elements:
.balloon {
background-color: transparent;
border: none;
cursor: pointer;
display: block;
font-size: 4em;
height: 1em;
list-style-type: none;
margin: 0;
padding: 0;
text-align: center;
-webkit-user-select: none; /* Safari fallback */
user-select: none;
width: 1em;
}
One problem with the balloons is that some of them are intentionally doing nothing at all. That’s because the popovers they close are not open. The player might think they didn’t click/tap that particular balloon or that the game is broken, so let’s add a little scaling while the balloon is in its :active
state of clicking:
.balloon:active {
scale: 0.7;
transition: 0.5s;
}
Bonus: Because the cursor
is a hand pointing its index finger, clicking a balloon sort of looks like the hand is poking the balloon with the finger. 👉🎈💥
The way we distribute the balloons around the screen is another important thing to consider. We’re unable to position them randomly without JavaScript so that’s out. I tried a bunch of things, like making up my own “random” numbers defined as custom properties that can be used as multipliers, but I couldn’t get the overall result to feel all that “random” without overlapping balloons or establishing some sort of visual pattern.
I ultimately landed on a method that uses a class to position the balloons in different rows and columns — not like CSS Grid or Multicolumns, but imaginary rows and columns based on physical insets. It’ll look a bit Grid-like and is less “randomness” than I want, but as long as none of the balloons have the same two classes, they won’t overlap each other.
I decided on an 8×8 grid but left the first “row” and “column” empty so the balloons are clear of the browser’s left and top edges.
/* Rows */
.r1 { --row: 1; }
.r2 { --row: 2; }
/* all the way up to .r7 */
/* Columns */
.c1 { --col: 1; }
.c2 { --col: 2; }
/* all the way up to .c7 */
.balloon {
/* This is how they're placed using the rows and columns */
top: calc(12.5vh * (var(--row) + 1) - 12.5vh);
left: calc(12.5vw * (var(--col) + 1) - 12.5vw);
}
Congratulating The Player (Or Not)
We have most of the game pieces in place, but it’d be great to have some sort of victory dance popover to congratulate players when they successfully pop all of the balloons in time.
Everything goes back to a
open>
element. Once that element is not open
, the game should be over with the last step being to pop that final balloon. So, if we give that element an ID of, say, #root
, we could create a condition to hide it with display: none
when it is :not()
in an open
state:#root:not([open]) {
display: none;
}
This is where it’s great that we have the :has()
pseudo-selector because we can use it to select the #root
element’s parent element so that when #root
is closed we can select a child of that parent — a new element with an ID of #congrats
— to display a faux popover displaying the congratulatory message to the player. (Yes, I’m aware of the irony.)
#game:has(#root:not([open])) #congrats {
display: flex;
}
If we were to play the game at this point, we could receive the victory message without popping all the balloons. Again, manual popovers won’t close unless the correct button is clicked — even if we close its ancestral
element.
Is there a way within CSS to know that a popover is still open? Yes, enter the :popover-open
pseudo-class.
The :popover-open
pseudo-class selects an open popover. We can use it in combination with :has()
from earlier to prevent the message from showing up if a popover is still open on the page. Here’s what it looks like to chain these things together to work like an and
conditional statement.
/* If #game does *not* have an open #root
* but has an element with an open popover
* (i.e. the game isn't over),
* then select the #congrats element...
*/
#game:has(#root:not([open])):has(:popover-open) #congrats {
/* ...and hide it */
display: none;
}
Now, the player is only congratulated when they actually, you know, win.
Conversely, if a player is unable to pop all of the balloons before a timer expires, we ought to inform the player that the game is over. Since we don’t have an if()
conditional statement in CSS (not yet, at least) we’ll run an animation for one minute so that this message fades in to end the game.
#fail {
animation: fadein 0.5s forwards 60s;
display: flex;
opacity: 0;
z-index: -1;
}
@keyframes fadein {
0% {
opacity: 0;
z-index: -1;
}
100% {
opacity: 1;
z-index: 10;
}
}
But we don’t want the fail message to trigger if the victory screen is showing, so we can write a selector that prevents the #fail
message from displaying at the same time as #congrats
message.
#game:has(#root:not([open])) #fail {
display: none;
}
We need a game timer
A player should know how much time they have to pop all of the balloons. We can create a rather “simple” timer with an element that takes up the screen’s full width (100vw
), scaling it in the horizontal direction, then matching it up with the animation above that allows the #fail
message to fade in.
#timer {
width: 100vw;
height: 1em;
}
#bar {
animation: 60s timebar forwards;
background-color: #e60b0b;
width: 100vw;
height: 1em;
transform-origin: right;
}
@keyframes timebar {
0% {
scale: 1 1;
}
100% {
scale: 0 1;
}
}
Having just one point of failure can make the game a little too easy, so let’s try adding a second
element with a second “root” ID, #root2
. Once more, we can use :has
to check that neither the #root
nor #root2
elements are open
before displaying the #congrats
message.
#game:has(#root:not([open])):has(#root2:not([open])) #congrats {
display: flex;
}
Wrapping up
The only thing left to do is play the game!
Fun, right? I’m sure we could have built something more robust without the self-imposed limitation of a JavaScript-free approach, and it’s not like we gave this a good-faith accessibility pass, but pushing an API to the limit is both fun and educational, right?
I’m interested: What other wacky ideas can you think up for using popovers? Maybe you have another game in mind, some slick UI effect, or some clever way of combining popovers with other emerging CSS features, like anchor positioning. Whatever it is, please share!