Iāve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.
You may have seen other tutorials out there showing off what the Popover API can do, but this is more of a beating-it-mercilessly-into-submission kind of article. Weāll add a little more pop music to the mix, like with balloonsā¦ some literal āpopā if you will.
What Iāve done is make a game ā using only HTML and CSS, of course ā leaning on the Popover API. Youāre tasked with popping as many balloons as possible in under a minute. But be careful! Some balloons are (as Gollum would say) ātricksyā and trigger more balloons.
I have cleverly called it Pop(over) the Balloons and weāre going to make it together, step by step. When weāre done itāll look something like (OK, exactly like) this:
CodePen Embed Fallback
Handling the popover attribute
Any element can be a popover as long as we fashion it with the popover attribute:
<div popover>…</div>
We donāt even have to supply popover with a value. By default, popoverās initial value is auto and uses what the spec calls ālight dismiss.ā That means the popover can be closed by clicking anywhere outside of it. And when the popover opens, unless they are nested, any other popovers on the page close. Auto popovers are interdependent like that.
The other option is to set popover to a manual value:
<div popover=āmanualā>…</div>
ā¦which means that the element is manually opened and closed ā we literally have to click a specific button to open and close it. In other words, manual creates an ornery popup that only closes when you hit the correct button and is completely independent of other popovers on the page.
CodePen Embed Fallback
Using the <details> element as a starter
One of the challenges of building a game with the Popover API is that you canāt load a page with a popover already openā¦ and thereās no getting around that with JavaScript if our goal is to build the game with only HTML and CSS.
Enter the <details> element. Unlike a popover, the <details> element can be open by default:
<details open>
<!– rest of the game –>
</details>
If we pursue this route, weāre able to show a bunch of buttons (balloons) and āpopā all of them down to the very last balloon by closing the <details>. In other words, we can plop our starting balloons in an open <details> element so they are displayed on the page on load.
This is the basic structure Iām talking about:
<details open>
<summary>š</summary>
<button>š</button>
<button>š</button>
<button>š</button>
</details>
In this way, we can click on the balloon in <summary> to close the <details> and āpopā all of the button balloons, leaving us with one balloon (the <summary> at the end (which weāll solve how to remove a little later).
You might think that <dialog> would be a more semantic direction for our game, and youād be right. But there are two downsides with <dialog> that wonāt let us use it here:
The only way to close a <dialog> thatās open on page load is with JavaScript. As far as I know, there isnāt a close <button> we can drop in the game that will close a <dialog> thatās open on load.
<dialog>s are modal and prevent clicking on other things while theyāre open. We need to allow gamers to pop balloons outside of the <dialog> in order to beat the timer.
Thus we will be using a <details open> element as the gameās top-level container and using a plain olā <div> for the popups themselves, i.e. <div popover>.
All we need to do for the time being is make sure all of these popovers and buttons are wired together so that clicking a button opens a popover. Youāve probably learned this already from other tutorials, but we need to tell the popover element that there is a button it needs to respond to, and then tell the button that there is a popup it needs to open. For that, we give the popover element a unique ID (as all IDs should be) and then reference it on the <button> with a popovertarget attribute:
<!– Level 0 is open by default –>
<details open>
<summary>š</summary>
<button popovertarget=”lvl1″>š</button>
</details>
<!– Level 1 –>
<div id=”lvl1″ popover=”manual”>
<h2>Level 1 Popup</h2>
</div>
This is the idea when everything is wired together:
CodePen Embed Fallback
Opening and closing popovers
Thereās a little more work to do in that last demo. One of the downsides to the game thus far is that clicking the <button> of a popup opens more popups; click that same <button> again and they disappear. This makes the game too easy.
We can separate the opening and closing behavior by setting the popovertargetaction attribute (no, the HTML spec authors were not concerned with brevity) on the <button>. If we set the attribute value to either show or hide, the <button> will only perform that one action for that specific popover.
<!– Level 0 is open by default –>
<details open>
<summary>š</summary>
<!– Show Level 1 Popup –>
<button popovertarget=”lvl1″ popovertargetaction=”show”>š</button>
<!– Hide Level 1 Popup –>
<button popovertarget=”lvl1″ popovertargetaction=”hide”>š</button>
</details>
<!– Level 1 –>
<div id=”lvl1″ popover=”manual”>
<h2>Level 1 Popup</h2>
<!– Open/Close Level 2 Poppup –>
<button popovertarget=”lvl2″>š</button>
</div>
<!– etc. –>
Note, that Iāve added a new <button> inside the <div> that is set to target another <div> to pop open or close by intentionally not setting the popovertargetaction attribute on it. See how challenging (in a good way) it is to āpopā the elements:
CodePen Embed Fallback
Styling balloons
Now we need to style the <summary> and <button> elements the same so that a player cannot tell which is which. Note that I said <summary> and not <details>. Thatās because <summary> is the actual element we click to open and close the <details> 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 to none on the <summary> element to get rid of the triangular marker that indicates whether the <details> is open or closed. That marker is really useful and great to have by default, but for a game like this, it would be better to remove that hint for a better challenge.
Safari doesnāt like that same approach. To remove the <details> marker here, we need to set a special vendor-prefixed pseudo-element, summary::-webkit-details-marker to display: none.
Itād be good if the mouse cursor indicated that the balloons are clickable, so we can set cursor: pointer on the <summary> elements as well.
One last detail is setting the user-select property to none on the <summary>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 <button> and <summary> 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;
}
CodePen Embed Fallback
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);
}
CodePen Embed Fallback
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 <details 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 <details> 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 <details> 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!
CodePen Embed Fallback
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!