I’ve at all times been fascinated with how a lot we will do with simply HTML and CSS. The brand new interactive options of the Popover API are one more instance of simply how far we will get with these two languages alone.
You’ll have seen different tutorials on the market exhibiting off what the Popover API can do, however that is extra of a beating-it-mercilessly-into-submission form of article. We’ll add a bit extra pop music to the combo, like with balloons… some literal “pop” if you’ll.
What I’ve carried out is make a recreation — utilizing solely HTML and CSS, in fact — leaning on the Popover API. You’re tasked with popping as many balloons as potential in below a minute. However watch out! Some balloons are (as Gollum would say) “tricksy” and set off extra balloons.
I’ve cleverly referred to as it Pop(over) the Balloons and we’re going to make it collectively, step-by-step. After we’re carried out it’ll look one thing like (OK, precisely like) this:
popover
attribute
Dealing with the Any ingredient generally is a popover so long as we style it with the popover
attribute:
<div popover>...</div>
We don’t even have to produce popover
with a price. By default, popover
‘s preliminary worth is auto
and makes use of what the spec calls “gentle dismiss.” Meaning the popover may be closed by clicking anyplace outdoors of it. And when the popover opens, except they’re nested, some other popovers on the web page shut. Auto popovers are interdependent like that.
The opposite possibility is to set popover
to a guide
worth:
<div popover=“guide”>...</div>
…which signifies that the ingredient is manually opened and closed — we actually should click on a particular button to open and shut it. In different phrases, guide
creates an ornery popup that solely closes while you hit the proper button and is totally impartial of different popovers on the web page.
<particulars>
ingredient as a starter
Utilizing the One of many challenges of constructing a recreation with the Popover API is that you may’t load a web page with a popover already open… and there’s no getting round that with JavaScript if our aim is to construct the sport with solely HTML and CSS.
Enter the <particulars>
ingredient. In contrast to a popover, the <particulars>
ingredient may be open by default:
<particulars open>
<!-- remainder of the sport -->
</particulars>
If we pursue this route, we’re in a position to present a bunch of buttons (balloons) and “pop” all of them right down to the final balloon by closing the <particulars>
. In different phrases, we will plop our beginning balloons in an open <particulars>
ingredient so they’re displayed on the web page on load.
That is the fundamental construction I’m speaking about:
<particulars open>
<abstract>🎈</abstract>
<button>🎈</button>
<button>🎈</button>
<button>🎈</button>
</particulars>
On this approach, we will click on on the balloon in <abstract>
to shut the <particulars>
and “pop” all the button balloons, leaving us with one balloon (the <abstract>
on the finish (which we’ll clear up tips on how to take away a bit later).
You may assume that <dialog>
can be a extra semantic route for our recreation, and also you’d be proper. However there are two downsides with <dialog>
that gained’t allow us to use it right here:
- The one method to shut a
<dialog>
that’s open on web page load is with JavaScript. So far as I do know, there isn’t an in depth<button>
we will drop within the recreation that can shut a<dialog>
that’s open on load. <dialog>
s are modal and stop clicking on different issues whereas they’re open. We have to enable players to pop balloons outdoors of the<dialog>
to be able to beat the timer.
Thus we will probably be utilizing a <particulars
open>
ingredient as the sport’s top-level container and utilizing a plain ol’ <div>
for the popups themselves, i.e. <div popover>
.
All we have to do in the interim is make sure that all of those popovers and buttons are wired collectively in order that clicking a button opens a popover. You’ve most likely realized this already from different tutorials, however we have to inform the popover ingredient that there’s a button it wants to answer, after which inform the button that there’s a popup it must open. For that, we give the popover ingredient a singular ID (as all IDs ought to be) after which reference it on the <button>
with a popovertarget
attribute:
<!-- Degree 0 is open by default -->
<particulars open>
<abstract>🎈</abstract>
<button popovertarget="lvl1">🎈</button>
</particulars>
<!-- Degree 1 -->
<div id="lvl1" popover="guide">
<h2>Degree 1 Popup</h2>
</div>
That is the thought when every little thing is wired collectively:
Opening and shutting popovers
There’s a bit extra work to do in that final demo. One of many downsides to the sport so far is that clicking the <button>
of a popup
opens extra popups; click on that very same <button>
once more and so they disappear. This makes the sport too simple.
We will separate the opening and shutting habits by setting the popovertargetaction
attribute (no, the HTML spec authors weren’t involved with brevity) on the <button>
. If we set the attribute worth to both present
or cover
, the <button>
will solely carry out that one motion for that particular popover.
<!-- Degree 0 is open by default -->
<particulars open>
<abstract>🎈</abstract>
<!-- Present Degree 1 Popup -->
<button popovertarget="lvl1" popovertargetaction="present">🎈</button>
<!-- Disguise Degree 1 Popup -->
<button popovertarget="lvl1" popovertargetaction="cover">🎈</button>
</particulars>
<!-- Degree 1 -->
<div id="lvl1" popover="guide">
<h2>Degree 1 Popup</h2>
<!-- Open/Shut Degree 2 Poppup -->
<button popovertarget="lvl2">🎈</button>
</div>
<!-- and so on. -->
Notice, that I’ve added a brand new <button>
contained in the <div>
that’s set to focus on one other <div>
to pop open or shut by deliberately not setting the popovertargetaction
attribute on it. See how difficult (in a great way) it’s to “pop” the weather:
Styling balloons
Now we have to model the <abstract>
and <button>
components the identical so {that a} participant can not inform which is which. Notice that I stated <abstract>
and not <particulars>
. That’s as a result of <abstract>
is the precise ingredient we click on to open and shut the <particulars>
container.
Most of that is fairly commonplace CSS work: setting backgrounds, padding, margin, sizing, borders, and so on. However there are a few vital, not essentially intuitive, issues to incorporate.
- First, there’s setting the
list-style-type
property tonone
on the<abstract>
ingredient to eliminate the triangular marker that signifies whether or not the<particulars>
is open or closed. That marker is absolutely helpful and nice to have by default, however for a recreation like this, it will be higher to take away that trace for a greater problem. - Safari doesn’t like that very same method. To take away the
<particulars>
marker right here, we have to set a particular vendor-prefixed pseudo-element,abstract::-webkit-details-marker
toshow: none
. - It’d be good if the mouse cursor indicated that the balloons are clickable, so we will set
cursor: pointer
on the<abstract>
components as effectively. - One final element is setting the
user-select
property tonone
on the<abstract>
s to forestall the balloons — that are merely emoji textual content — from being chosen. This makes them extra like objects on the web page. - And sure, it’s 2024 and we nonetheless want that prefixed
-webkit-user-select
property to account for Safari help. Thanks, Apple.
Placing all of that in code on a .balloon
class we’ll use for the <button>
and <abstract>
components:
.balloon {
background-color: clear;
border: none;
cursor: pointer;
show: block;
font-size: 4em;
peak: 1em;
list-style-type: none;
margin: 0;
padding: 0;
text-align: heart;
-webkit-user-select: none; /* Safari fallback */
user-select: none;
width: 1em;
}
One drawback with the balloons is that a few of them are deliberately doing nothing in any respect. That’s as a result of the popovers they shut should not open. The participant may assume they didn’t click on/faucet that exact balloon or that the sport is damaged, so let’s add a bit scaling whereas the balloon is in its :lively
state of clicking:
.balloon:lively {
scale: 0.7;
transition: 0.5s;
}
Bonus: As a result of the cursor
is a hand pointing its index finger, clicking a balloon kind of appears just like the hand is poking the balloon with the finger. 👉🎈💥
The best way we distribute the balloons across the display is one other vital factor to think about. We’re unable to place them randomly with out JavaScript in order that’s out. I attempted a bunch of issues, like making up my very own “random” numbers outlined as customized properties that can be utilized as multipliers, however I couldn’t get the general outcome to really feel all that “random” with out overlapping balloons or establishing some kind of visible sample.
I in the end landed on a way that makes use of a category to place the balloons in numerous rows and columns — not like CSS Grid or Multicolumns, however imaginary rows and columns primarily based on bodily insets. It’ll look a bit Grid-like and is much less “randomness” than I need, however so long as not one of the balloons have the identical two lessons, they gained’t overlap one another.
I made a decision on an 8×8 grid however left the primary “row” and “column” empty so the balloons are away from the browser’s left and prime edges.
/* Rows */
.r1 { --row: 1; }
.r2 { --row: 2; }
/* all the best way as much as .r7 */
/* Columns */
.c1 { --col: 1; }
.c2 { --col: 2; }
/* all the best way as much as .c7 */
.balloon {
/* That is how they're positioned utilizing the rows and columns */
prime: calc(12.5vh * (var(--row) + 1) - 12.5vh);
left: calc(12.5vw * (var(--col) + 1) - 12.5vw);
}
Congratulating The Participant (Or Not)
Now we have a lot of the recreation items in place, however it’d be nice to have some kind of victory dance popover to congratulate gamers once they efficiently pop all the balloons in time.
All the pieces goes again to a <particulars
open>
ingredient. As soon as that ingredient is not open
, the sport ought to be over with the final step being to pop that last balloon. So, if we give that ingredient an ID of, say, #root
, we may create a situation to cover it with show: none
when it’s :not()
in an open
state:
#root:not([open]) {
show: none;
}
That is the place it’s nice that we have now the :has()
pseudo-selector as a result of we will use it to pick the #root
ingredient’s guardian ingredient in order that when #root
is closed we will choose a toddler of that guardian — a brand new ingredient with an ID of #congrats
— to show a pretend popover displaying the congratulatory message to the participant. (Sure, I’m conscious of the irony.)
#recreation:has(#root:not([open])) #congrats {
show: flex;
}
If we had been to play the sport at this level, we may obtain the victory message with out popping all of the balloons. Once more, guide popovers gained’t shut except the proper button is clicked — even when we shut its ancestral <particulars>
ingredient.
Is there a approach inside CSS to know {that a} popover remains to be open? Sure, enter the :popover-open
pseudo-class.
The :popover-open
pseudo-class selects an open popover. We will use it together with :has()
from earlier to forestall the message from exhibiting up if a popover remains to be open on the web page. Right here’s what it appears prefer to chain this stuff collectively to work like an and
conditional assertion.
/* If #recreation does *not* have an open #root
* however has a component with an open popover
* (i.e. the sport is not over),
* then choose the #congrats ingredient...
*/
#recreation:has(#root:not([open])):has(:popover-open) #congrats {
/* ...and conceal it */
show: none;
}
Now, the participant is simply congratulated once they truly, you already know, win.
Conversely, if a participant is unable to pop all the balloons earlier than a timer expires, we ought to tell the participant that the sport is over. Since we don’t have an if()
conditional assertion in CSS (not but, no less than) we’ll run an animation for one minute in order that this message fades in to finish the sport.
#fail {
animation: fadein 0.5s forwards 60s;
show: flex;
opacity: 0;
z-index: -1;
}
@keyframes fadein {
0% {
opacity: 0;
z-index: -1;
}
100% {
opacity: 1;
z-index: 10;
}
}
However we don’t need the fail message to set off if the victory display is exhibiting, so we will write a selector that stops the #fail
message from displaying similtaneously #congrats
message.
#recreation:has(#root:not([open])) #fail {
show: none;
}
We’d like a recreation timer
A participant ought to understand how a lot time they should pop all the balloons. We will create a slightly “easy” timer with a component that takes up the display’s full width (100vw
), scaling it within the horizontal route, then matching it up with the animation above that enables the #fail
message to fade in.
#timer {
width: 100vw;
peak: 1em;
}
#bar {
animation: 60s timebar forwards;
background-color: #e60b0b;
width: 100vw;
peak: 1em;
transform-origin: proper;
}
@keyframes timebar {
0% {
scale: 1 1;
}
100% {
scale: 0 1;
}
}
Having only one level of failure could make the sport a bit too simple, so let’s strive including a second <particulars>
ingredient with a second “root” ID, #root2
. As soon as extra, we will use :has
to test that neither the #root
nor #root2
components are open
earlier than displaying the #congrats
message.
#recreation:has(#root:not([open])):has(#root2:not([open])) #congrats {
show: flex;
}
Wrapping up
The one factor left to do is play the sport!
Enjoyable, proper? I’m positive we may have constructed one thing extra strong with out the self-imposed limitation of a JavaScript-free method, and it’s not like we gave this a good-faith accessibility cross, however pushing an API to the restrict is each enjoyable and academic, proper?
I’m : What different wacky concepts are you able to assume up for utilizing popovers? Perhaps you may have one other recreation in thoughts, some slick UI impact, or some intelligent approach of mixing popovers with different rising CSS options, like anchor positioning. No matter it’s, please share!