DEV Community

Jane Ori
Jane Ori

Posted on

GETting your IP Address with CSS - and other 32 bit API responses - without JavaScript!

Just over a week ago while scrolling YouTube shorts after work on a Friday, looking for something to want, this 35 second video stopped me and took 100% of my attention

After one and a half playthroughs, I paused it, tilted my head back and stared off into space as ideas rushed into my mind.

"I could make Chess in CSS with the CPU Hack."

"Could I connect it to that tablebase without JS?"

"The only way CSS can reach out to a server is with background ima---!!!

It can also use url() as content on a pseudo element, which can resize the parent, then inset: 0px an absolute position container inside of it and I can measure that with tan(atan2()) aaand that's my GET request's response data...

If the element resizes based on that content, it would be useful to have it off screen to avoid causing scrollbars - plus better DX for anyone using would if it didn't need a weird one-off setup that you had to build inside of... So how do I get the data out --

I've known how to lift data to :root since the CPU Hack since it already does that on a much smaller scale but never had a reason to try..."

Then I paused as my eyes darted around without any awareness of my physical surroundings, looking for edge cases among the components I've assembled - "What can't be done here?"

...

"I'd have to @container style(--x: N) wrap my urls so gamestate can choose what ones to use but I'd only have a hardcoded list of possible endpoints until we get url() param()...
AH WAIT! I could also generate the necessary request url and prompt the player to SMEAR it into the page, shrouded as the UI to confirm their move..."

I leaned back and splayed my fingers and palms, panning my eyes across the room slowly as excitement built inside me.

"16 bits on the width and 16 bits on the height is huge but well within possible..."

"I think that's it! I can do it!"

The rest of this plot exposition isn't directly relevant, but I'm enjoying the moment so I'll leave the writing here for anyone reading along

So I started building my chess board in CodePen, where all my ideas start their journey into realization.

"I guess it's time to learn Chess so I know how to build the API request and to determine if ~32 bits was enough response data for it to communicate the CPU move..."

I played Chess briefly back in my teenage IRC-loving, game hacking days and quit at the end of a losing streak with hysterical laughter when I devised a way to cheat every move of a match and still lost. (I made my friend go first then used the move he wrote in IRC against the "expert" computer player in the chess game I downloaded, then communicated to him what the CPU did as my move.)

I was COMPLETELY oblivious to how much of Chess I knew absolutely nothing about.

I searched for existing APIs I could just wrap, assuming I'll generate an image on my server use the height/width as the response data. I quickly found chess-api.com which can respond with a next move given "FEN" as input.

I learned FEN, which lead to even more discovery of how much I didn't know about Chess. I knew I could generate the SMEAR Hack to build the gamestate in FEN format, and most importantly, I measured that I could indeed bitpack the important response data move: "b7b8q" into 32 bits. (It's an 8x8 board and I know where everything is, not much fundamentally has to be provided in the response.)

Now, I know with more certainty I can program Chess against a CPU Player in 100% CSS.

I also knew at that moment, I should definitely rewatch The Queen's Gambit because I absolutely loved it and can't wait to pick up on new details.

The universe is talking to me and I'm putting the excitement into action to keep the loop flowing.

Time to test the CSS API URL GET ALL CAPS TRAIN idea and see if it works.

Demoing the basics

First, establish appropriate background noise for this endeavor, The Queen's Gambit.

Next, confirm measuring an image.

Easy enough! Just make sure all the content inside stays absolute so it doesn't change the size of the container holding the image.

Throw in a test for changing what URL we GET...

Proof of concept confirmed, next leg of this journey was lifting the response data to :root so I can make all the setup invisible and at least 65535px off screen to the top and left.

I knew this was going to be heavy, esoteric CSS+HTML and I make things that help people make things, so right from the start, I built this with the intention of making it an easy-to-use library.

In version 1, Copy plop some specific (bulky) HTML in your page with a handful of options built in to make the requests and do the :root lift for you.

Import the library and specify the API endpoint from your CSS,

@import url(https://unpkg.com/css-api-fetch@1/api-fetch.css);

@container style(--api-id: 1) {
  .api-fetch { --api-fetch: url(https://css-api.propjockey.io/ip-address.php); }
}
Enter fullscreen mode Exit fullscreen mode

And the data will be on :root for the rest of your app once it's ready.

But it gets better, fast!

First Public Demonstration, Chrome Only, :root

GETting a user's IP Address with CSS.

Screenshot of the initial state of the codepen linked

I'm not going to go into super specific implementation details in this post of how exactly that CSS+HTML lifts the data to :root in that original public demo because a couple of CSS loving friends pointed me in a much easier direction I hadn't learned about!

but I will put this quick summary here for anyone interested

:root:has(.specific-element:hover) { ..state... }

If the specific-element descendant knows it's :hover, it can change to put a different element on top of where it currently is which causes the next element to be hovered, then :root:has(...:hover) changes its state based on that element too, in a loop.

The CPU Hack lets us accumulate the state as different elements are :hover'd.

So if the data we want to lift is '512', we can sequence 'thousands', 'hundreds', 'tens', and 'ones', telling the child to present an element 0 to 9 representing the value it's holding in that position.

.api-transfer-value-0:hover, ...5:hover, 1, 2.

Child knows what numeric position we're asking for because state cascades, :has() lifts the value because the child presented the corresponding digit for us to :hover and read at :root.

The CPU Hack accumulates it into one value then tells the child to stop reporting after the ones position is saved.

The HTML in the demo linked above reveals a bit of what setup is required for that. The CSS code for it is here in v1 of css-api-fetch.

Second Public Demonstration - Cross Browser, no :root

The only difference here is that I don't lift it to :root and instead leave it in the container, DX requires you to wrap all of your content that needs access to the data in an absolutely positioned element inside of the hack's context.

In my original exposition I mentioned that I wanted to avoid this scenario, but it's cross browser and it has its uses too. Plus, it's not like you can't build your whole app inside the specific html setup.

You can see in the HTML tab just how much less effort is involved in there.

Just 4 specific nested html elements are required in the cross-browser version, then you build anything you want using your response data inside of that.

Third Public Demonstration - Chrome Only, :root, trivial

Kizu mentioned it might be possible to simplify so I went digging, then after not quite putting the dots together, T. Afif showed a possible way in that direction, I learned a ton from that article but was unsatisfied with the division and precision loss so I dove into the articles and amazing tools Bramus made and came up with a variation that satisfied my use cases.

As you can see in the CodePen's HTML the setup is so simple it's literally one line of HTML to fetch and your response data is on :root. In this case I used a second element to present the data too. AMAZING!

I packaged it up for a v3 release making all the handoff mindless, putting the measuring stuff off screen already set up since it's not important.

I posted about the release, then T. Afif linked a different article demonstrating a similar view-timeline configuration for measuring that I worked out for this - without the division! :)

I highly recommend reading all the work I've linked above, there's some exciting, talented CSS people sharing their ideas -- and knowing something is possible opens entire worlds of cool ideas!

What is css-api-fetch doing?

Importing css-api-fetch and adding a single line of html, you can fairly easily do API requests in your CSS and not have to consider the complexity of view-timeline yourself. I made it so you don't have to think about it, but for the curious, here's the concept and setup the library uses internally:

  1. Two animations setting a --ints from the max values your API will respond with to 0
  2. host (scope) the animations and their timelines on :root
  3. create a measuring element sized to the max values your API will respond with, with non-static position, overflow hidden, etc to make it suitable for reading view/scroll positions.
  4. Throw it absolute offscreen to the top left so it doesn't cause scrollbars on your app.
  5. load the image url() inside of a nested size container's pseudo element's content and set view-timelines for both inline and block corresponding to the animations in step 1&2.
  6. The animations now report the size of the image to the whole document

Here's a demonstration of all 6 of the steps above, step 4 is commented out so you can see it. We'll use Lorem Picsum to emulate specific height & width api responses easily.

That's the fundamentals but the library sets up 4 separate API request/responses that you can use simultaneously without knowing how it works.

What's css-bin-bits?

All 3 of the public releases have demonstrations that use a library I made ages ago, css-bin-bits that makes it super easy to do bitwise operations on 16 bit integers in 100% CSS and you don't usually have to write any calc() yourself - the library takes care of it all. For the 32 bit IP Address, using css-bin-bits I was able to limit the size of the image to exactly 16 bits on a single dimension.

Without bitpacking the 32 bit IP Address into two 16 bit values, T. Afif demonstrates you could use a 255255px by 255255px measuring container and split the response data out with decimal math - skipping css-bin-bits. css-api-fetch supports you setting any maximum, shipping with 99999 as the default maximum, much more than 16 bits (65535), but not quite 255255 lol.

How do you set up an API to respond with data in the image?

The lightest and most trivial way to pack data into the width and height of an image is to make the server respond with a bare-minimum SVG.

Here's how the IP address is packed and served in PHP, for example:

<?php
  header('Content-type: image/svg+xml');

  $adr = $_SERVER['REMOTE_ADDR'] ?: '255.255.255.255';
  $hex = str_pad(implode(array_map('dechex', explode('.', preg_replace('/[^\d\.]/', '', $adr), 4))), 8, '0', STR_PAD_LEFT);
  /* rx returns first 4 hex chars, removing up to 3 leading 0's */
  $width = hexdec(preg_replace('/(^0{0,3})|(.{4}$)/', '', $hex) ?: '0');
  /* rx returns the last 4 hex chars, removing up to 3 leading 0's */
  $height = hexdec(preg_replace('/^.{4}0{0,3}/', '', $hex) ?: '0');

  echo '<svg xmlns="http://www.w3.org/2000/svg" width="' . $width . 'px" height="' . $height . 'px"></svg>';
?>
Enter fullscreen mode Exit fullscreen mode

The API requests you set up could easily accomplish a handful of low hanging fruit that CSS can't otherwise do without JavaScript

  • RNG
  • Current Time
  • TimeZone
  • Country Code
  • Operating System
  • or connection to any other api if you wrap it - such as chess-api

What comes next?

Chess?!

Screenshot of an early build of an unreleased Chess game with a bit of code on the left indicating specific openings such as king's pawn and queen's pawn. The board shows an opening move has been played by white, it's the Reti Opening, Nf3

Not yet! Plus, Lilian has already made Chess in CSS!

I'm not sure how it works or if there's a publicly available demo, but I'm sure she'd answer any questions! :)


For now, my excitement is focused elsewhere......

I want more than 32 bits per request.

I want 512 bits per request.

Gif showing a bunch of integer data being set one at a time in a harsh terminal appearance

No JavaScript.


Open Contact 👽

Please do reach out if you need help with any of this, have feature requests, want to share what you've created, or wish to learn more.

PropJockey.io CodePen DEV Blog GitHub Mastodon
PropJockey.io CodePen DEV Blog GitHub Mastodon

🦋@JaneOri.PropJockey.io

𝕏@Jane0ri

Top comments (1)

Collapse
 
bilal__b7b27da085c profile image
Bilal Kureshi

Banger