Curious Geckos 🦎🦎🦎: The Most Precise CSS-only Position-Aware Mini-Game!
Tags: webdev, css, frontend, html
Published: 2024-03-27 13:15:00

#### A **CSS-only,** no JS, no checkbox, **position-aware** (hover/touch aware) interactive gecko-luring pastime!

&nbsp;

> <big>**TL;DR**</big> I revisited an old CSS-only hack with new CSS features and some ingenuity to bypass rendering limitations and achieve as much as a **25.6x precision gain** over the best previous solution – getting much closer to native position awareness! Try out the [mini-game](https://codepen.io/warkentien2/full/jOJJwEp), play with just the [“debounced CSS algorithm,”](https://codepen.io/warkentien2/pen/oNmwLdy) and if you are in a rush, start reading from [Section 2.](#the-concept)

---

## Intro

We're gathered here today to gain **awareness** about an ingenious CSS trick I call **CSS-only Position Awareness.** We could also call it Position-Aware CSS, but it fails to unambiguously state that no JavaScript was used.

It's based on a known trick, but, as you'll soon find out, CSS `:has` enabled us to push it further. And so we did. Besides, **AI** still has no clue about it and maybe neither do you. Here's what **ChatGPT** had to say this morning – March 27th, 2024:

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wk4dmto3iudmam75zfsv.png" alt="Screenshot of ChatGPT, see caption below">
  <figcaption>transcription:<br>
   👨 — Is it possible to achieve position-awareness with only CSS and HTML?"<br>🤖 — <b>"No, it's not possible</b> to achieve position-awareness using only CSS and HTML. JavaScript is necessary to interact with the mouse events and update CSS properties or HTML content based on the mouse's position."</figcaption>
</figure>

This article will be **interactive** and hopefully inspiring. So, stick around! _(Or at least scroll to see the **cute photos**)_

---

## Table of Contents

1. [The Idea](#the-idea)
2. [Position Awareness](#the-concept)
3. [Building a Position-Aware Game](#how-did-i-build-this)
4. [Cast and Credits](#cast-and-credits)
5. [Previous Art](#previous-art)
6. [FAQ](#faq)

---

## 1. The idea <a name="the-idea"></a>

Back in **September 2023,** I was custom styling a `<table>` component and ran into a fun use case for the `:has()` pseudo-class. Without resorting to a "CSS-grid table," but using the actual – correct and accessible – `<table>` markup, I could now highlight not only the row but also the column being hovered!

See it in action:
{% codepen https://codepen.io/warkentien2/pen/ExGZLLe %}

> <small>_:bulb:: **[ Fun fact ]** [Sten Hougaard](https://twitter.com/netsi1964) discovered this same trick a month later and got [featured](https://www.instagram.com/p/C0fp9WhNvM7/) on CodePen's Instagram. :clap:_</small>

**What stood out was** how little CSS I needed to write. All it took was 37 lines of pure CSS to highlight a 7-column table by any given number of rows. What if I needed more columns? Could I refactor this CSS algorithm to support any number of columns? While digging for this answer **I stumbled upon something else,** a better algorithm for **CSS-only Position Awareness.** And that's where the game comes in.

Before we get all scientific, **let's play** the mini-game!

&nbsp;

[![Click to play Curious Geckos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3un03r334rd05w9a6jq6.png)](https://codepen.io/warkentien2/full/jOJJwEp)

&nbsp;

> <small>_:orange_book:: Supported on all devices and modern browsers. For the best mouse-tracking UX try Chrome Desktop (or Edge Desktop)_</small>

This was all achieved without a single line of JavaScript! You're welcome to double-check and disable JavaScript on your browser.

---

## 2. Position Awareness <a name="the-concept"></a>

A code is **position-aware** when we can pinpoint the `--x` and `--y` coordinates of a cursor on the screen and make these values available for any element on the page. To achieve this without JavaScript requires a position grid to detect which subsection of the screen is being hovered. Ideally, we would have a position cell per pixel, but we aren't there yet. We might, however, be just a Moore's Law cycle away.

Previous iterations of the Position Grid were modest, only tracking 4 quadrants and calling it **Direction Awareness.** It evolved into **10x10** grids (100 cells) called **Mouse Tracking,** some as large as **16x16**. However, Mouse Tracking was not the best name for something that should include track pads and touch devices. Now it evolves once more, with the addition of `:has` and dynamic rendering. I present to you the **Position Awareness** grid, with <big><big>**81x81**, **6561 cells**!</big></big>

There's a catch to Position Awareness: we change the expected behavior of hovering on a page. Instead of tracking `:hover` interactions with DOM elements, we are tracking interactions with subsections of the screen.

Here's a “debounced” Position-Aware CSS algorithm in action:
{% codepen https://codepen.io/warkentien2/pen/oNmwLdy %}

> <small>_:computer_mouse::computer: : Intended for mouse and trackpad devices._</small>
>
> <small>_:orange_book:: 81x81 grid for Desktop, 27x27 grid mobile_</small>

&nbsp;

### 2.a.: Precision Gains <a name="2-a"></a>

**Previous approach** was achieved by placing non-nested cells before a target element, and transferring placement coordinates through the “any sibling ahead” selector `.cell:hover ~ .position-aware-element`. This overloads the wrapper component with a huge number of child nodes and can impact performance.

Here's a brute-force image generated by painting each pixel with CSS to illustrate what happens when we style too many elements. **Hit [play](https://codepen.io/warkentien2/pen/ZEMKdpq) at your peril :scream:.**
![Brute force image generated with CSS, painting a pixel grid with CSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kubaxyl9s2sx6orla4yt.png)

**Current approach** takes advantage of the `:has()` pseudo-class which supports nested position cells. By nesting a single level deep we reduce the max number **N** of children cells to the **square root of N**. E.g.: a previous shallow position grid with 100 children cells can now be represented with 10 children, each with 10 children. This way, every single cell has only at most 10 children. Imagine if we nest even deeper! This two-level approach hits a performance cap at around **33x33** (1089 points of awareness). This is the resolution I'm using for **Curious Geckos.** Beyond that limit I would need to create dynamically rendered grid subsections, like I showcased on the blue Position-Aware CSS example above. This is similar to how 3D game engines dynamically increase the level of detail on elements near the camera.

> <small>_:orange_book:: More on this in a future article_</small>

---

## 3. Building a Position-Aware Game <a name="how-did-i-build-this"></a>

Now that we have a Position-Aware Grid filled with `(x, y)` coordinates, how do we go from `--x` and `--y` values to a working mini-game?

Here's the trimmed version of the steps:

<strong><big>3.1.:</big></strong> Feed `--x` and `--y` coordinates into the :fly:(fly) and :lizard:(gecko) wrappers
```scss
@for $i from 1 through 33 {
   .position-aware-container:has(tr:nth-child(#{$i}):hover) {
      :is(.fly__wrapper, .gecko__placement) {
      --y: #{($i - 1) * $axis-step};
      }
   }

   .position-aware-container:has(td:nth-child(#{$i}):hover) {
      :is(.fly__wrapper, .gecko__placement) {
      --x: #{($i - 1) * $axis-step};
      }
   }
}
```

<strong><big>3.2.:</big></strong> Use CSS `calc` to rotate the :fly:(fly) by a centrifugal angle
![angles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tv8qqhg4z4w2epq8pl5.jpg)

<strong><big>3.3.:</big></strong> Create SVG puppets for the geckos:
![SVG Puppet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/im54e3wfpw8c20bkyxw9.png)

<small><strong>· 3.3.a.:</strong></small> A Puppet, also known as a Rig Model, is a technique used for animation. You might have seen it before on my [portfolio](https://warkentien2.com/)
![Tarantula](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkx1u5trg8e7w3x5y9h9.gif)
> <small>_:orange_book:: More on this in a future article_</small>

<small><strong>· 3.3.b.:</strong></small> Group each body part in a hierarchical structure. **E.g.:** If I rotate the shoulder, the entire arm rotates.

<small><strong>· 3.3.c.:</strong></small> Anchor geckos by their thorax to facilitate head angle calculation:
![Trigonometry diagram showcasing how the Gecko's head rotation angle is being calculated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3qn81qpizi31aect5nb8.png)
```scss
--head-angle: max(
   var(--min-head-angle),
   // limit CCW head rotation
   min(
      var(--max-head-angle),
      // limit CW head rotation
      calc(
         (atan2(var(--dy), var(--dx)) * var(--head-angle-intensity, 1)) - calc(
               (
                  var(--head-initial-rotation, 0) * var(
                     --head-angle-intensity,
                     1
                     ) * 1deg
               ) + var(--head-angle-correction, 0deg)
            )
         )
   )
);
```

<small><strong>· 3.3.d.:</strong></small> Split the head rotation in three for a more natural motion:
```scss
.gecko__neck__body {
   --rotate-member: calc(var(--head-angle) * 1 / 6);
}
.gecko__neck__head {
   --rotate-member: calc(var(--head-angle) * 1 / 3);
}
.gecko__head {
   --rotate-member: calc(var(--head-angle) * 1 / 2);
}
```

<strong><big>3.4.:</big></strong> Set attack zones per gecko and per @media query.
![Attack zones: position grid cells that trigger the gecko's attack animation when hovered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ht3aywuimhcjsua40xm1.png)

<strong><big>3.5.:</big></strong> Detect attack zone `:hover` and trigger attack state
```scss
// ----- Gecko 1 attacks ----- //
:is(
   .gecko-trap-1_base,
   .gecko-trap-1_min200,
   .gecko-trap-1_min250,
   .gecko-trap-1_min300,
   .gecko-trap-1_min350,
   .gecko-trap-1_min450,
   .gecko-trap-1_max166,
   .gecko-trap-1_max150,
   .gecko-trap-1_max125,
   .gecko-trap-1_max100,
   .gecko-trap-1_max75,
   .gecko-trap-1_max66,
   .gecko-trap-1_max50
) {
body:has(& span:hover) {
```
- Expand the hovered cell to full-screen. This locks hover to the last hovered cell. Locking the attack state.
- Attack state freezes fly movement and animates the desired gecko.

<strong><big>3.6.:</big></strong> Slide in a pulsating call-to-action with a higher `z-index`, this will trigger the restart state.

<strong><big>3.7.:</big></strong> Add some flourishes:
- tail animations
- fine-tuned gecko placements to best fit the aspect ratio
- Black and White gecko's resting arm changes position based on the @media query for best composition

<strong><big>3.8.:</big></strong> You're done!

---

## 4. Cast and Credits <a name="cast-and-credits"></a>

<figure>
   <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cs2cxkmkg3ikbtje0eiy.png" alt="cast">
   <figcaption>
      <pre>
      Super Hypo Tangerine Leopard Gecko - by <a href="https://www.deviantart.com/geekusmaximus/art/Leopard-Gecko-2-107247050" target="_blank">Tamara Locke</a>
      Oreo Whiteout African Fat-Tailed Gecko — by unknown
      Mandarin Tangerine Leopard Gecko — by <a href="https://www.cbreptile.com/wp-content/uploads/2018/09/mandarin-leopard-gecko-for-sale.jpg" target="_blank">CB Reptile</a>
      </pre>
   </figcaption>
</figure>

- Geckos — SVGs designed in Figma — by **warkentien2**
  - Scales by [pattern.monster](https://pattern.monster/leaves-4/), thanks [Naveen CS](https://twitter.com/MrNaveenCS)!
- Fly - [CSS art](https://codepen.io/warkentien2/pen/abPGgyZ) – by **warkentien2**
- First sketch ![Curious Geckos mockup on paper](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0mjrhcdj9rnqublo01u4.jpeg)

---

## 5. Previous art <small>(Post-project research)</small> <a name="previous-art"></a>

Although many developers have tackled some form of Position Awareness, I would like to highlight a few:

1. [Fabrice Weinberg](https://twitter.com/FWeinb), June 2013: **2x2** (first ever?) Direction-Aware Grid [(Code)](https://codepen.io/FWeinb/pen/nxdYZN)
2. [Gabrielle Wee](https://twitter.com/gabriewee), Jan 2017: 4 cells. Beautiful direction-aware effect. [(Code)](https://codepen.io/gabriellewee/pen/Qdpgwx)
3. Gabrielle Wee, April 2018: **10x10** grid with 100 individual `<a>` tags and 100 individual style definitions. [(Code)](https://codepen.io/gabriellewee/pen/mLyaZL)
> "It would be smoother with more links but also would take longer to load, so I only used 100 instead of something like 1000."
> – Gabrielle Wee's notes on this Pen
4. Christopher Joshua, Nov 2019: **16x16** grid with 256 individual `<i>` tags and 768 lines of CSS just for tracking. [(Code)](https://codepen.io/xiell/pen/JjjxoQz)
5. **Honorable Mention <sup>(1)</sup>** [Jane Ori](https://twitter.com/Jane0ri)'s amazing etch-a-sketch with an impressive **75x50** grid! [(Code)](https://codepen.io/propjockey/pen/mdBvjgg)
   <big>Genius</big> state tracking with the [space toggle hack](https://lea.verou.me/blog/2020/10/the-var-space-hack-to-toggle-multiple-values-with-one-custom-property/) + animations.
   &nbsp;&nbsp; ______________________________________ &nbsp;&nbsp;
   <br/>
   **(1): Honorable mention** since it achieves Position Tracking but not Position Awareness.
   <small><strong>· 5.a.:</strong></small> Position information is dynamically set, not extracted.
   <small><strong>· 5.b.:</strong></small> Cell can't be hovered twice.
   <small><strong>· 5.c.:</strong></small> Regardless, it's impressive and deserves to be shared!

---

## 6. FAQ <a name="faq"></a>

1. Is this really a game?
   Well, it's as much of a game as playing catch is a game. :baseball: It's the reason I called it a mini-game <small>:baseball:</small> or a pastime.
2. Why make this?
   To Flex... and to Grid.
3. What do you mean by “no checkbox”?
   - Most CSS-only games use checkbox for state management, but not this one.
   - This game runs completely on `:hover` events, even to track victory/restart.
4. UX is jittery, can I improve it?
   **Troubleshooting:**
   - Try Chrome Desktop or Edge Desktop for the best UX.
     - If you're a MacBook user, you can emulate Chrome inside Safari:
       `Safari > Develop (tab) > Open Page With > Google Chrome`
   - Use a [JS disabler (extension)](https://chromewebstore.google.com/detail/quick-javascript-switcher/geddoclleiomckbhadiaipdggiiccfje).
   - If it's still slow, use Firefox or Safari Desktop for a debounced hover tracking experience.

&nbsp;
&nbsp;

<big><big>Thank you for reading!</big></big>

You're welcome to ask questions and speak your mind.

Follow me on **X**, [@warkentien2](https://twitter.com/warkentien2)