9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

A Pure CSS 'mousemove' Detector with :has() #17

Open 9am opened 8 months ago

9am commented 8 months ago
Build a 'mousemove' event listener with just CSS.
sensor hits
9am commented 8 months ago

Table of contents


Preface

Let's take a moment to think about how to implement this on the web:

demo

It probably goes like this:

  1. Create a series of boxes with descending sizes in HTML.
  2. Place them concentrically with CSS, like position: absolute.
  3. Add a mousemove event listener with JavaScript, in the handler, and move those boxes according to the offset value to the center position.

But, is it possible to do it without JavaScript? In other words:

For an effect relying on the position of MouseEvent, is there a way to track the 'mouse' using just CSS?


𐄡


## Build The Sensor ### The MouseEvent in CSS We're looking for something in CSS that can be changed by `MouseEvent`, which gives us only 1 option: **Pseudo-classes**, specifically [User action pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes#user_action_pseudo-classes). > For example, the style under `button:active` kicks in when the user presses down the button. When comes to the `mousemove`, `:hover` is what we need. Consider this: On top of the target, we can apply a sensor layer, which is composed of small sensor cells. While the cursor moves inside the target, the sensor cells under the cursor will be `:hover-ed` accordingly. If we can collect the `index` of the `:hover-ed` cell, it can be used to calculate the `x` `y` coordinates, like the `MouseEvent` could give us. Let's build a 2*2 sensor: ```html

``` ```css .sensor { --col: 2; --row: 2; display: grid; grid-template-columns: repeat(var(--col), 1fr); grid-template-rows: repeat(var(--row), 1fr); } ``` > ![step-1](https://github.com/9am/9am.github.io/assets/1435457/d548b0a3-8a65-4714-8273-8e82e321e783) > > [![Edit step-1](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/step-1-tlg2jr?fontsize=14&hidenavigation=1&module=%2Fmain.css&theme=dark)
### The Power of `:has()` The problem for the solution is how to collect the `index` in CSS. The answer is [`:has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has). Known as the 'parent selector', `:has()` provides the unique power of defining style by the children's behavior. Suppose we have a 4-cell sensor, the `index` of the active cell can be collected with a custom variable `--n`. ```css .sensor:has(.cell:nth-child(1):hover) { --n: 0; } .sensor:has(.cell:nth-child(2):hover) { --n: 1; } .sensor:has(.cell:nth-child(3):hover) { --n: 2; } .sensor:has(.cell:nth-child(4):hover) { --n: 3; } .sensor { --n: /* will be the index of 'active' cell while the cursor moving around */ } ``` > ![step-2](https://github.com/9am/9am.github.io/assets/1435457/aaba1ecf-89be-4e5a-92d6-bfeea2e93f0a) > > [![Edit step-2](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/step-2-tsxfmn?fontsize=14&hidenavigation=1&module=%2Fmain.css&theme=dark)
### Get The (x,y) Coordinates from Sensor Now we have the `--n`, it's easy to get the `(x, y)` with `--col`. > `--x: mod(var(--n), var(--col))` > `--y: round(down, var(--n) / var(--col))` Here we use CSS functions `mod()` and `round()`, since they are not supported widely, we'll consider replacing them with tricks that work like them: ```css @property --x { syntax: ""; initial-value: 0; inherits: true; } @property --y { syntax: ""; initial-value: 0; inherits: true; } --y: calc(var(--n) / var(--col) - 0.5); --x: calc(var(--n) - var(--col) * var(--y)); ``` > ![step-3](https://github.com/9am/9am.github.io/assets/1435457/c1e2b60f-e403-45ca-96a4-7b99146bb802) > > [![Edit step-3](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/step-3-4t3pts?fontsize=14&hidenavigation=1&module=%2Fmain.css&theme=dark)
### Put Them Together Now we can increase the number of cells for a more accurate sensor, and test it with a blue ball placed in the middle of the active cell. > ![step-4](https://github.com/9am/9am.github.io/assets/1435457/421887a5-ad10-42b3-9219-a58d79496c88) > > The 8*8 demo > Edit in [Codepen](https://codepen.io/9am/details/zYygxKq)

𐄡


## Build The Effect Now get back to the effect at the beginning. To replace step-3, instead of writing JavaScript, insert the sensor. Now we have the `(--x,--y)`, throw some `perspective-3d` movement, and we can do it with pure CSS. > ![demo](https://github.com/9am/9am.github.io/assets/1435457/f6e76803-1fbf-4464-b112-e8bdc54405db) > > Edit in [Codepen](https://codepen.io/9am/pen/MWZNaMM)

𐄡


## Closing thoughts The drawback of this solution is the redundancy of similar declarations for the sensor. There is no way to get rid of them currently unless something like `counter-value()` can be used with `calc()`, which has always been the feature I wanted the most for CSS. There is a discussion about this [here](https://github.com/w3c/csswg-drafts/issues/1026). But in cases we can not use `JavaScript`, and don't need it to be super accurate, it's always good to know we have options.
--- > ## @9am 🕘 > * Read more [articles](https://9am.github.io) at [9am.github.io](https://9am.github.io) > * Find other [things](https://www.npmjs.com/search?q=%409am) I built on [GitHub](https://github.com/9am) and [NPM](https://www.npmjs.com/~9am) > * Contact me via [email](mailto:tech.9am@gmail.com) > * [![Creative Commons License](https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/)