Limited-Time Special Use Code to Get 30% Off Your First Buying EduBank. Find out more!
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:
<div id="game">
<div id="timer">
<div id="bar"></div>
</div>
<div id="instructions">
<div class="msg">
<h1>Pop(over) the Balloons</h1>
You have 2 minutes to pop as many balloons as possible. But it's not as simple as it sounds. Some
balloons
create more balloons and some balloons pop other balloons.
<a class="btn" href="#game">Go!</a>
</div>
</div>
<details open id="root1">
<summary class="balloon r4 c5">🎈</summary>
<button class="balloon r2 c2" popovertarget="pop1" popovertargetaction="show">🎈</button>
<details open>
<summary class="balloon r6 c6">🎈</summary>
<button class="balloon r1 c4" popovertarget="pop5" popovertargetaction="show">🎈</button>
</details>
<button class="balloon r1 c5" popovertarget="pop3" popovertargetaction="show">🎈</button>
<details open>
<summary class="balloon r2 c6">🎈</summary>
<button class="balloon r7 c3" popovertarget="pop4" popovertargetaction="show">🎈</button>
<button class="balloon r3 c4" popovertarget="pop1" popovertargetaction="hide">🎈</button>
</details>
</details>
<details open id="root2">
<summary class="balloon r4 c6">🎈</summary>
<button class="balloon r3 c7" popovertarget="pop2" popovertargetaction="show">🎈</button>
<details open>
<summary class="balloon r2 c3">🎈</summary>
<button class="balloon r6 c1" popovertarget="pop1" popovertargetaction="show">🎈</button>
</details>
<button class="balloon r5 c4" popovertarget="pop4" popovertargetaction="show">🎈</button>
<details open>
<summary class="balloon r7 c7">🎈</summary>
<button class="balloon r5 c2" popovertarget="pop1" popovertargetaction="show">🎈</button>
<button class="balloon r3 c1" popovertarget="pop2" popovertargetaction="hide">🎈</button>
</details>
</details>
<div id="pop1" popover="manual">
<button class="balloon r4 c2" popovertarget="pop1" popovertargetaction="hide">🎈</button>
</div>
<dialog id="pop2" popover="manual">
<button class="balloon r3 c3" popovertarget="pop2" popovertargetaction="hide">🎈</button>
<button class="balloon r1 c1" popovertarget="pop4" popovertargetaction="hide">🎈</button>
</dialog>
<div id="pop3" popover="manual">
<button class="balloon r5 c5" popovertarget="pop4" popovertargetaction="hide">🎈</button>
<button class="balloon r6 c2" popovertarget="pop3" popovertargetaction="hide">🎈</button>
</div>
<div id="pop4" popover="manual">
<button class="balloon r7 c2" popovertarget="pop4" popovertargetaction="hide">🎈</button>
<button class="balloon r6 c5" popovertarget="pop2" popovertargetaction="hide">🎈</button>
</div>
<div id="pop5" popover="manual">
<button class="balloon r4 c7" popovertarget="pop4" popovertargetaction="hide">🎈</button>
<button class="balloon r5 c3" popovertarget="pop5" popovertargetaction="hide">🎈</button>
</div>
<div id="congrats">
<div class="msg">
You popped them all!<br>
<a class="btn" href="">Play Again!</a>
</div>
</div>
<div id="fail">
<div class="msg">
Unlike Pokemon, you didn't pop 'em all...<br>
Refresh your browser to try again.<br>
<a class="btn" href="">Play Again!</a>
</div>
</div>
</div>
body {
margin: 0;
background-color: lightskyblue;
}
details,
div {
inset: unset;
overflow: visible;
border: none;
background-color: transparent;
padding: 0;
}
.r1 {
--row: 1;
}
.r2 {
--row: 2;
}
.r3 {
--row: 3;
}
.r4 {
--row: 4;
}
.r5 {
--row: 5;
}
.r6 {
--row: 6;
}
.r7 {
--row: 7;
}
.c1 {
--col: 1;
}
.c2 {
--col: 2;
}
.c3 {
--col: 3;
}
.c4 {
--col: 4;
}
.c5 {
--col: 5;
}
.c6 {
--col: 6;
}
.c7 {
--col: 7;
}
.balloon {
position: absolute;
display: block;
font-size: 4em;
border: none;
background-color: transparent;
padding: 0;
margin: 0;
height: 1em;
width: 1em;
top: calc(12.5vh * (var(--row) + 1) - 12.5vh);
left: calc(12.5vw * (var(--col) + 1) - 12.5vw);
list-style-type: none;
cursor: pointer;
text-align: center;
user-select: none;
scale: 1;
}
.balloon:active {
scale: 0.7;
transition: 0.5s;
}
details summary::-webkit-details-marker {
display: none;
}
#root1:not([open]),
#root2:not([open]) {
display: none;
}
#congrats,
#fail,
#instructions {
display: none;
align-items: center;
justify-content: center;
inset: 0;
position: absolute;
background-color: hsla(0, 0%, 0%, 0.5);
}
#fail {
display: flex;
opacity: 0;
z-index: -1;
}
#game:target #fail {
animation: 0.5s fadein forwards 60s;
}
#game:target #bar {
animation: 60s timebar forwards;
}
#game:target #instructions {
display: none;
}
#instructions {
display: flex;
z-index: 10;
}
@keyframes fadein {
0% {
opacity: 0;
z-index: -1;
}
100% {
opacity: 1;
z-index: 10;
}
}
.msg {
background-color: #e60b0b;
color: #fff;
padding: 1em;
border-radius: 0.5em;
border: 0.25em solid #760b0b;
overflow: hidden;
max-width: 20em;
}
#congrats .msg .btn,
#fail .msg .btn {
float: none;
display: block;
}
.msg h1 {
margin-top: 0;
}
#game:has(#root1:not([open])):has(#root2:not([open])) #congrats {
display: flex;
}
#game:has(#root1:not([open])):has(#root2:not([open])) #fail,
#game:has(#root1:not([open])):has(#root2:not([open])) #bar {
display: none;
}
#game:has(#root1:not([open])):has(:popover-open):has(#root2:not([open])):has(:popover-open)
#fail {
display: flex;
}
#game:has(#root1:not([open])):has(:popover-open):has(#root2:not([open])):has(:popover-open)
#bar {
display: block;
}
#game:has(#root1:not([open])):has(:popover-open):has(#root2:not([open])):has(:popover-open)
#congrats {
display: none;
}
.btn {
float: right;
background-color: white;
color: #e60b0b;
text-decoration: none;
padding: 0.25em 0.5em;
margin-top: 0.5em;
text-align: center;
border-radius: 0.5em;
}
#timer {
width: 100vw;
height: 1em;
}
#bar {
background-color: #e60b0b;
width: 100vw;
height: 1em;
transform-origin: right;
}
@keyframes timebar {
0% {
scale: 1 1;
}
100% {
scale: 0 1;
}
}
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.
<button popovertarget="autopop">Auto</button>
<div id="autopop" popover="auto"><h1>Click anywhere to close me</h1></div>
<button popovertarget="manpop">Manual</button>
<div id="manpop" popover="manual"><h1>Ha ha! You can't close me!</h1><p>(unless you click the button again)</p></div>