CSS or BS

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

CSS or BS game in progress. The player is asked to declare whether 'view-timeline-name' is a real CSS property or made-up.

Well this is a fun (and frustrating!) game. You’ll be presented with 20 (alleged) CSS properties, but some of them… are convincing-looking fakes! You’ve got 10 seconds to identify whether each is real or not. Every few you get right increases the difficulty level, but also the score potential. How high can you score?

Me? Oh, I kept getting up into the “forbidden” level and then my brain would melt and I’d crash out. Quite proud of my last run, though:

Final score: 61/80. Reached: Forbidden. "If CSS knowledge were currency, you'd be comfortably middle-class."

×

Dynamically-Deployed Static Site Subdomains on Caddy

I’ve recently been experimenting with where I host my small and open-source static sites. In my latest experiment, I wanted to try a low-maintenance selfhosting solution1. Here’s what I wanted:

  • Pushing to the main branch of my GitHub/Codeberg/wherever repo would send a webook to my server.
  • Upon receiving the webhook, my server would pull the latest changes2.
  • Using a wildcard certificate, my webserver automatically mounts each project at a subdomain matching its project name3.

Here’s what I came up with:

Step 1: webhook handler

I’m using Caddy as my webserver, because despite its considerable power and versatility it’s a breeze to set up. To sort wildcard DNS later I’ll want to swap in a custom build, but to get started I just ran apt install caddy. Then I used apt install webhook to install Adnan Hajdarević’s webhook endpoint, and tied the two together in my Caddyfile:

webhook.duckling.danq.me {
  reverse_proxy localhost:9000
}
My static server’s called duckling.danq.me, so you’ll see that turn up a lot in these configs.

Then I created a webhook in a GitHub repository:

GitHub webhook pointing to https://webhook.duckling.danq.me/hooks/github-push, with a secret set and SSL verification enabled, triggered by a push event.
I generated a long random string to use as the secret, and kept a copy for later.

When you create a webhook in GitHub it immediately sends a test event, but it doesn’t quite look like a real push event so I pushed an inconsequential change to the repo to trigger another. Once you’ve got a “real” one sent, you can re-send it via the “Recent Deliveries” tab as many times as you like, to help with testing.

Then, on the server, I checked-out a copy of the code (anonymously: this is a public repository so I don’t need keys to read from it anyway) and set up my /etc/webhook.conf to expect these calls:

[
    {
      "id": "github-push",
      "execute-command": "/var/www/github-push/webhook.sh",
      "command-working-directory": "/var/www/github-push/",
      "pass-arguments-to-command": [
        {
          "source": "payload",
          "name": "repository.name"
        }
      ],
      "trigger-rule": {
        "and": [
          {
            "match": {
              "type": "payload-hash-sha256",
              "secret": "[MY SECRET KEY HERE]",
              "parameter": {
                "source": "header",
                "name": "X-Hub-Signature-256"
              }
            }
          },
          {
            "match": {
              "type": "value",
              "value": "refs/heads/main",
              "parameter": {
                "source": "payload",
                "name": "ref"
              }
            }
          }
        ]
      }
    }
  ]
  
The trigger-rule directives ensure that (a) the secret key is correct (it uses a HMAC hash across the entire JSON request, so it prevents payload tampering too) and (b) the event only triggers on pushes to the main branch. The execute-command specifies the Bash script I want to run when the webhook is triggered. The pass-arguments-to-command configuration says to send the repo name on to that script.

Now all I needed to do was write the /var/www/github-push/webhook.sh Bash script so that it pulled the latest copy of the code when triggered:

#!/bin/bash
cd /var/www/github-push/$1 && git pull

I was able to test this by pushing inconsequential changes to my codebase and watching them get replicated down to my webserver. Neat!

Step 2: low-maintenance webserver

After pointing the DNS for *.static.duckling.danq.me at my static server, I set about configuring Caddy to be able to use DNS-01 challenges to get itself wildcard SSL certificates4. Caddy can’t do DNS-01 challenges out of the box, so you either need to write your own renewal script or compile Caddy with plugins corresponding to your DNS provider. My domains’ DNS are managed by a mixture of AWS Route 53, Gandi, and Namecheap, so my xcaddy build step looked like this:

xcaddy build \
  --with github.com/caddy-dns/route53 \
  --with github.com/caddy-dns/gandi \
  --with github.com/caddy-dns/namecheap

Of course, if I’d have preferred somebody else build it for me, CaddyServer’s download configurator would have done it for me on-demand.

For Gandi and Namecheap I just need a personal access token or API key, respectively, but Route 53’s configuration is slightly more-involved: I needed to create a new user via IAM and give it permission to write DNS TXT records for the appropriate hosted zone. Fortunately the guide for the caddy-dns/route53 repo had an almost copy-pastable example.

I added the AWS access key and secret key as environment variables (like this!) into my /etc/systemd/system/multi-user.target.wants/caddy.service service definition, and then told my Caddyfile to make use of them when renewing the wildcard certificate:

*.static.duckling.danq.me {
    tls {
        dns route53 {
          access_key_id {env.AWS_ACCESS_KEY_ID}
          secret_access_key {env.AWS_SECRET_ACCESS_KEY}
        }
      }
      root * /var/www/github-push/{http.request.host.labels.4}
      file_server
    }
}
The {http.request.host.labels.4} refers to the fourth part of the domain name, when separated at the dots and counted from the right, so 0 = me, 1 = danq, 2 = duckling, 3 = static, and 4 = the part that we’re interested in. So long as I don’t store any other directories in the /var/www/github-push/ directory then this will simply map each subdomain onto its git repository name and return a 404 for any other request.

DNS-01 challenges are necessarily slower than HTTP-01/ALPN challenges, because they’re limited by DNS propogation, so it took a while before the certificate was issued. I ran Caddy in the foreground to watch the logs while it did so:

Caddy webserver logs, with a highlighted section showing a DNS-01 challenge for *.static.duckling.danq.me repeatedly fail and then eventually succeed, then a certificate chain being installed.

You can see the whole thing working (for now at least; I don’t know if I’m keeping this approach!) by going to e.g. embed-html.static.duckling.danq.me, which dynamically tracks the main branch of the embed-html repo on GitHub.

I don’t yet know if this is going to be the future forever-home of my many static site side projects, but it’s certainly been the most-satisfying experiment to run so-far.

Footnotes

1 I’ve drifted away from selfhosting simple static sites lately because I’ve accidentally broken them with configuration changes too many times! But I figured I’d be open to in-housing them again if I had a single simple architecture for them all, so I spun up a VPS and gave it a go

2 Running a build script or some other static site generation tool is out of scope for now, but I want to be able to confirm that it would be possible in the future.

3 It also needs to be possible for me to map other domain names to it, but that’s a triviality.

4 It’s absolutely possible to use tls { on_demand } to do this, but it’s better to use a wildcard certificate which can be pre-generated and doesn’t let people trick your server into making ludicrous numbers of certificate requests by hammering random subdomain names.

× ×

Moving a static site from GitHub to Codeberg Pages

Late to the party,1 I finally got around to experimentally moving a GitHub Pages-hosted static site to Codeberg. I wanted a low-risk site to try first, so I moved Beige Buttons, the site hosting my “90s PC turbo button simulator” web component.

Ê

Mostly for my own benefit later, here’s the steps I took and the things I learned along the way:

  1. You can migrate a repository across in about two clicks. Easy!
  2. Codeberg Pages is deployed from the pages branch. If there’s no build step to the static site, all you need to do is rename the main branch to pages (and probably make it the default branch).2
  3. The default URL is https://username.codeberg.page/repository.
  4. You can use a custom domain by adding a .domains file that lists domains; if migrating from GitHub Pages you can just rename your CNAME file to .domains.
  5. You’ll need to tweak your DNS CNAME, ALIAS (or, worst-case, A/AAAA) record to point at Codeberg Pages.3

Change propogation feels slightly slower than GitHub, but perfectly tolerable.

The one thing that’s causing me trouble is that Codeberg Pages’ CORS headers prevent people from hotlinking the Beige Buttons JS, so there are some projects for which this wouldn’t be a suitable migration (issues are raised). But for most static sites, it’d probably Just Work and seems to be a great alternative.

Footnotes

1 With thanks to Kev for reminding me I’d had this on my list.

2 There are other ways to deploy but they don’t support custom domains yet.

3 Like GitHub Pages, Codeberg Pages uses LetsEncrypt for certificate provision, so you don’t need to change any CAA records.

Predictions in a Hat

Two decades ago this month my friend Matt posted five predictions about the future of the world. I’ve revisited these predictions twice since: ten years later and twenty years later, and “scored” his predictions both times.

I love that the Web’s memory (and the persistence of URLs) makes this kind of long-term conversation possible.

A Random List of Silly Things I Hate

So apparently now this is a thing, so here I go:

  1. Websites that are just blank pages if the JavaScript doesn’t load from the CDN.1
  2. The misunderstanding that LLMs can somehow be a route to AGI.
  3. Computer systems that say my name is too short or my password is too long.2
  4. People being unwilling to discuss their wild claims later using the lack of discussion as evidence of widespread acceptance.
  5. When people balance the new toilet roll one atop the old one’s tube.3
A nearly-full roll of toilet paper perched atop an empty toilet roll tube on an open-ended spindle.
Come on! It would have been so easy!
  1. Shellfish. Why would you eat that!?
  2. People assuming my interest in computers and technology means I want to talk to them about cryptocurrencies.4
  3. Websites that nag you to install their shitty app. (I know you have an app. I’m choosing to use your website. Stop with the banners!)
  4. People who seem to only be able to drive at one speed.5
  5. The assumption that the fact I’m “sharing” my partner is some kind of compromise on my part; a concession; something that I’d “wish away” if I could. (It’s very much not.)
  6. Brexit.

Wow, that was strangely cathartic.

Footnotes

1 I have a special pet hate for websites that require JavaScript to render their images. Like… we’d had the <img> tag since 1993! Why are you throwing it away and replacing it with something objectively slower, more-brittle, and less-accessible?

2 Or, worse yet, claiming that my long, random password is insecure because it contains my surname. I get that composition-based password rules, while terrible (even when they’re correctly implemented, which they’re often not), are a moderately useful model for people to whom you’d otherwise struggle to explain password complexity. I get that a password composed entirely of personal information about the owner is a bad idea too. But there’s a correct way to do this, and it’s not “ban passwords with forbidden words in them”. Here’s what you should do: first, strip any forbidden words from the password: you might need to make multiple passes. Second, validate the resulting password against your composition rules. If it fails, then yes: the password isn’t good enough. If it passes, then it doesn’t matter that forbidden words were in it: a properly-stored and used password is never made less-secure by the addition of extra information into it!

3 This is the worst of the toilet paper crimes, but there’s a lesser but more-common offence.

4 Also: I’m uninterested in whatever multiplayer shooter game you’re playing, and no I won’t fix your printer.

5 “You were doing 35mph in the 60mph limit, then you were doing 35mph in the 40mph limit, now you’re doing 35mph in the 20mph limit. Argh!”

×

Why Security Engineering needs a Hacker Mentality

My recent post How an RM Nimbus Taught Me a Hacker Mentality kickstarted several conversations, and I’ve enjoyed talking to people about the “hacker mindset” (and about old school computers!) ever since.1

Thinking “like a hacker” involves a certain level of curiosity and creativity with technology. And there’s a huge overlap between that outlook and the attitude required to be a security engineer.

By way of example: I wrote a post for a Web forum2 recently. A feature of this particular forum is that (a) it has a chat room, and (b) new posts are “announced” to the chat room.

Simulated screenshot of the 'Liquid Crystal Cafe' chat room, in which user 'Shoutbox' has recently announced that 'Dan Q created Things I Have Been Breaking Into This Week...'.
It’s a cute and useful feature that the chat room provides instant links to new topics.

The title of my latest post contained a HTML tag (because that’s what the post was talking about). But when the post got “announced” to the chat room… the HTML tag seemed to have disappeared!

And this is where “hacker curiosity” causes a person to diverge from the norm. A normal person would probably just say to themselves “huh, I guess the chat room doesn’t show HTML elements in the subjects of posts it announces” and get on with their lives. But somebody with a curiosity for the technical, like me, finds themselves wondering exactly what went wrong.

It took only a couple of seconds with my browser’s debug tools to discover that my HTML tag… had actually been rendered to the page! That’s not good: it means that, potentially, the combination of the post title and the shoutbox announcer might be a vector for an XSS attack. If I wrote a post with a title of, say, <script src="//example.com/some-file.js"></script>Benign title, then the chat room would appear to announce that I’d written a post called “Benign title”, but anybody viewing it in the chat room would execute my JavaScript payload3.

I reached out to an administrator to let them know. Later, I delivered a proof-of-concept: to keep it simple, I just injected an <img> tag into a post title and, sure enough, the image appeared right there in the chat room.

Screenshot of the same forum, but the new shoutbox message contains an 88×31 animated GIF that says 'Dan Q', where the 'Q' spins on its axis.
Injecting an 88×31 seemed like a less-disruptive proof-of-concept than, y’know, alert('xss'); or something!

This didn’t start out with me doing penetration testing on the site. I wasn’t looking to find a security vulnerability. But I spotted something strange, asked “what can I make it do?”, and exercised my curiosity.

Even when I’m doing something more-formally, and poking every edge of a system to try to find where its weak points are… the same curiosity still sometimes pays dividends.

I remember that a decade ago, I found a vulnerability in a central IT system at the University of Oxford that involved setting a payload in a domain-wide cookie with the anticipation that a trusted system would trip over it later. The administrators of the system had been running the usual automated scanners to look for security problems, but those tools lack the imagination of a human with a hacker mindset.

And that’s why you need that mindset in your security engineers. Curiosity, imagination, and the willingness to ask “what can I make it do?”. Because if you don’t find the loopholes, the bad guys will.

Footnotes

1 It even got as far as the school run, where I ended up chatting to another parent about the post while our kids waited to be let into the classroom!

2 Remember forums? They’re still around, and – if you find one with the right group of people – they’re still delightful. They represent the slower, smaller communities of a simpler Web: they’re not like Reddit or Facebook where the algorithm will always find something more to “feed” you; instead they can be a place where you can make real human connections online, so long as you can deprogram yourself of your need to have an endless-scroll of content and you’re willing to create as well as consume!

3 This, in turn, could “act as” them on the forum, e.g. attempting to steal their credentials or to make them post messages they didn’t intend to, for example: or, if they were an administrator, taking more-significant actions!

× ×

I Am Experimenting with Blocking HTTP1.1

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

Most of the traffic I get on this site is bots – it isn’t even close. And, for whatever reason, almost all of the bots are using HTTP1.1 while virtually all human traffic is using later protocols.

I have decided to block v1.1 traffic on an experimental basis. This is a heavy-handed measure and I will probably modify my approach as I see the results.

# Return an error for clients using http1.1 or below - these are assumed to be bots
@http-too-old {
    not protocol http/2+
    not path /rss.xml /atom.xml # allow feeds
}
respond @http-too-old 400 {
    body "Due to stupid bots I have disabled http1.1. Use more modern software to access this site"
    close
}

This is quick, dirty, and will certainly need tweaking but I think it is a good enough start to see what effects it will have on my traffic.

A really interesting experiment by Andrew Stephens! And love that he shared the relevant parts of his Caddyfile: nice to see how elegantly this can be achieved.

I decided to probe his server with cURL:

~ curl --http0.9 -sI https://sheep.horse/ | head -n1
HTTP/2 200
~ curl --http1.0 -sI https://sheep.horse/ | head -n1
HTTP/1.0 400 Bad Request
~ curl --http1.1 -sI https://sheep.horse/ | head -n1
HTTP/1.1 400 Bad Request
~ curl --http2 -sI https://sheep.horse/ | head -n1
HTTP/2 200

Curiously, while his configuration blocks both HTTP/1.1 and HTTP/1.0, it doesn’t seem to block HTTP/0.9! Whaaa?

It took me a while to work out why this was. It turns out that cURL won’t do HTTP/0.9 over https:// connections. Interesting! Though it presumably wouldn’t have worked anyway – HTTP/1.1 requires (and HTTP/1.0 permits) the Host: header, but HTTP/0.9 doesn’t IIRC, and sheep.horse definitely does require the Host: header (I tested!).

I also tested that my RSS reader FreshRSS was still able to fetch his content. I have it configured to pull not only the RSS feed, which is specifically allowed to bypass his restriction, but – because his feed contains only summary content – I also have it fetch the linked page too in order to get the full content. It looks like FreshRSS is using HTTP/2 or higher, because the content fetcher still behaves properly.

Andrew’s approach definitely excludes Lynx, which is a bit annoying and would make this idea a non-starter for any of my own websites. But it’s still an interesting experiment.

Introduction to the new HTML element ‘geolocation’

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

The <geolocation> element provides a button that, when activated, prompts the user for permission to access their location. Originally, it was designed as a general <permission> element, but browser vendors indicated that implementing a “one-size-fits-all” element would be too complex. The result was a single-purpose element, probably the first of several.

<geolocation>
  <strong>Your browser doesn't support &lt;geolocation&gt;. Try Chrome 144+</strong>
</geolocation>

I’ve been waiting for this one. Given that “requesting permission to access a user’s location” has always required user intervention, at least to begin with, it makes sense to me that it would exist as a form control, rather than just as a JavaScript API.

Implementing directly in HTML means that it degrades gracefully in the standard “if you don’t understand an element, simply render its contents” way that the Web always has. And it’s really easy to polyfill support in for the new element so you can start using it today.

My only niggle with <geolocation> is that it still requires JavaScript. It feels like a trick’s been missed, there. What I’d have really wanted would have been <input type="geolocation">. This would e.g. renders as a button but when clicked (and permission granted) gets the user’s device location and fills the field (presumably with a JSON object including any provided values, such as latitude, longitude, altitude, accuracy, provider, and so on). Such an element would still provide all the same functionality of the new element, but would also be usable in a zero-JS environment, just like <input type="file">, <input type="datetime-local"> and friends.

This is still a huge leap forward and I look forward to its more-widespread adoption. And meanwhile, I’ll be looking into integrating it into both existing applications that use it and using it in future applications, by preference over the old API-driven approach. I’m grateful to Manuel for sharing what he’s learned!

Mocking SharePoint

Highlight of my workday was debugging an issue that turned out to be nothing like what the reporter had diagnosed.

The report suggested that our system was having problems parsing URLs with colons in the pathname, suggesting perhaps an encoding issue. It wasn’t until I took a deep dive into the logs that I realised that this was a secondary characteristic of many URLs found in customers’ SharePoint installations. And many of those URLs get redirected. And SharePoint often uses relative URLs when it sends redirections. And it turned out that our systems’ redirect handler… wasn’t correctly handling relative URLs.

It all turned into a hundred line automated test to mock SharePoint and demonstrate the problem… followed by a tiny two-line fix to the actual code. And probably the most-satisfying part of my workday!

The internet’s hidden creative renaissance (and how to find it)

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

A screenshot of a retro-styled website interface showing "the indie web" in yellow text on a purple rounded banner. The background displays various vintage web elements including a calculator, browser windows, website layouts, and navigation elements typical of early internet design. The URL shown is cameronsworld.net and the overall aesthetic mimics 1990s web design with colorful, eclectic interface elements scattered across the page.

Have you ever wished there were more to the internet than the same handful of apps and sites you toggle between every day? Then you’re in for a treat.

Welcome to the indie web, a vibrant and underrated part of the internet, aesthetically evocative of the late 1990s and early 2000s. Here, the focus is on personal websites, authentic self-expression, and slow, intentional exploration driven by curiosity and interest.

These kinds of sites took a backseat to the mainstream web around the advent of big social media platforms, but recently the indie web has been experiencing a revival, as more netizens look for connection outside the walled gardens created by tech giants. And with renewed interest comes a new generation of website owner-operators, intent on reclaiming their online experience from mainstream social media imperatives of growth and profit.

want to like this article. It draws attention to the indieweb, smolweb, independent modern personal web, or whatever you want to call it. It does so in a way that inspires interest. And by way of example, it features several of my favourite retronauts. Awesome.

But it feels painfully ironic to read this article… on Substack!

Substack goes… let’s say half-way… to representing the opposite of what the indieweb movement is about! Sure, Substack isn’t Facebook or Twitter… but it’s still very much in the same place as, say, Medium, in that it’s a place where you go if you want other people to be in total control of your Web presence.

The very things that the author praises of the indieweb – its individuality and personality, its freedom control by opaque corporate policies, its separation from the “same handful of apps and sites you toggle between every day” – are exactly the kinds of things that Substack fails to provide.

It’s hardly the biggest thing to hate about Substack, mind – that’d probably be their continued platforming of Covid conspriacy theorists and white nationalist hate groups. But it’s still a pretty big irony to hear the indieweb praised there!

Soo… nice article, shame about the platform it’s published on, I guess?

PHP 8.4 on Caddy on Debian 13… in Three Minutes

I just needed to spin up a new PHP webserver and I was amazed how fast and easy it was, nowadays. I mean: Caddy already makes it pretty easy, but I was delighted to see that, since the last time I did this, the default package repositories had 100% of what I needed!

Apart from setting the hostname, creating myself a user and adding them to the sudo group, and reconfiguring sshd to my preference, I’d done nothing on this new server. And then to set up a fully-functioning PHP-powered webserver, all I needed to run (for a domain “example.com”) was:

sudo apt update && sudo apt upgrade -y
sudo apt install -y caddy php8.4-fpm
sudo mkdir -p /var/www/example.com
printf "example.com {\n"                               | sudo tee    /etc/caddy/Caddyfile
printf "  root * /var/www/example.com\n"               | sudo tee -a /etc/caddy/Caddyfile
printf "  encode zstd gzip\n"                          | sudo tee -a /etc/caddy/Caddyfile
printf "  php_fastcgi unix//run/php/php8.4-fpm.sock\n" | sudo tee -a /etc/caddy/Caddyfile
printf "  file_server\n"                               | sudo tee -a /etc/caddy/Caddyfile
printf "}\n"                                           | sudo tee -a /etc/caddy/Caddyfile
sudo service caddy restart

After that, I was able to put an index.php file into /var/www/example.com and it just worked.

And when I say “just worked”, I mean with all the bells and whistles you ought to expect from Caddy. HTTPS came as standard (with a solid QualSys grade). HTTP/3 was supported with a 0-RTT handshake.

Mind blown.

Death to the shadow DOM!

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

A common rebuttal I get to this…

What about when you want to keep global styles out of your component, like with a third-party widget that gets loaded on lots of different pages?

I kind-of sort-of see the logic in that. But I also think wanting your component to not look like a cohesive part of the page its loaded into is weird and unexpected.

I so-rarely disagree with Chris on JavaScript issues, but I think I kinda do on this one. I fully agree that the Shadow DOM is usually a bad idea and its encapsulation concept encourages exactly the kind of over-narrow componentised design thinking that React also suffers from. But I think that the rebuttal Chris picks up on is valid… just sometimes.

When I created the Beige Buttons component earlier this year, I used the shadow DOM. It was the first time I’ve done so: I’ve always rejected it in my previous (HTML) Web Components for exactly the reasons Chris describes. But I maintain that it was, in this case, the right tool for the job. The Beige Buttons aren’t intended to integrate into the design of the site on which they’re placed, and allowing the site’s CSS to interact with some parts of it – such as the “reset” button – could fundamentally undermine the experience it intends to create!

I appreciate that this is an edge case, for sure, and most Web Component libraries almost certainly shouldn’t use the shadow DOM. But I don’t think it’s valid to declare it totally worthless.

That said, I’ve not yet had the opportunity to play with Cascade Layers, which – combined with directives like all: reset;, might provide a way to strongly override the style of components without making it impossibly hard for a site owner to provide their own customised experience. I’m still open to persuasion!

For anyone who :has :not yet seen the magic of modern CSS

Modern CSS is freakin’ amazing. Widespread support for nesting, variables, :has, and :not has unlocked so much potential. But I don’t yet see it used widely enough.

Suppose I have a form where I’m expecting, but not requiring, a user to choose an option from each of several drop-downs. I want to make it more visually-obvious which drop-downs haven’t yet had an option selected. Something like this:

<select name="guess[69427976b65e3]">
  <option></option>
  <option value="1">First answer</option>
  <option value="2">Second answer</option>
  <option value="3">Third answer</option>
</select>
This is close to an actual example generated as part of a Christmas quiz game I made the other year, and dusted off recently.

Suppose I want to style that <select> when the first, default, “empty” option is selected.

That could be as simple as this:

select:has(option:not([value]):checked) {
  outline: 6px dotted red;
}
It’s a slightly gnarly selector, but thanks to nesting you could choose to break it into multiple blocks if you preferred.

What that’s saying is:

  • a <select>
    • that contains an <option>
      • where that <option> does not have a value="..."
      • and that <option> is currently selected
  • gains a dotted red outline around it

Or in short: if the default option is selected, highlight it so the user knows they haven’t chosen a value yet. Sweet!

Animation showing a select box which receives a dotted red border each time its 'blank' first value is selected.
Obviously you could expand this to have different effects for every value, if you wanted.

I can’t understate how valuable it is that we can do this in CSS, nowadays. Compared to doing it in JavaScript… CSS gives better performance and reliability and is much easier to implement in a progressively-enhanced manner.

Here’s another example, this time using a fun “dress-up Dan” feature I from a redesign of my blog theme that I’m hoping to launch in the New Year:

If you’ve ever wanted to know what I’d look like if I were an elderly Tom Scott, my new design will answer that question!

Every single bit of interactivity shown in the video above… from the “waving” Dan to the popup menu to the emoji-styled checkboxes to the changes to t-shirt and hair colours… is implemented in CSS.

The underlying HTML is all semantic, e.g. the drop-down menu is a <details>/<summary> pair (with thanks to Eevee for the inspiration); its contents are checkbox and radiobutton <input>es; the images are SVGs that use CSS variables (another killer feature these years!) to specify colours (among other things), and virtually everything else… is CSS.

Consider this:

:root {
  /* Default values for Dan's t-shirt, hair, and beard colours used throughout the site: */
  --dan-tshirt: #c3d4d7;
  --dan-hair: #3b6f8f;
  --dan-beard: #896a51;

  /* ...more variables... */
}

/* When the page contains a "checked" checkbox, update some variables: */
:root:has(#dan-tshirt-color-white:checked)  { --dan-tshirt: #c3d4d7; }
:root:has(#dan-tshirt-color-purple:checked) { --dan-tshirt: #7429a8; }
/* ... */
:root:has(#dan-hair-color-natural:checked)  {   --dan-hair: #896a51; }
:root:has(#dan-hair-color-blue:checked)     {   --dan-hair: #3b6f8f; }

/* When "dye beard" is checked, copy the hair colour: */
:root:has(#dan-dye-beard-toggle:checked)    {  --dan-beard: var(--dan-hair); }
The ability to set :root CSS variables, based on the status of user-controlled elements like checkboxes within the document, unlocks amazing options for interactivity. It also works in smaller scopes like HTML Web Components, of course, for encapsulated functionality.

If you’re still using JavaScript for things like this, perhaps it’s time you looked at how much CSS has grown up this last decade or so. CSS gives you performance benefits, less fragility, and makes it easier for you to meet your accessibility and usability goals.

You can still enrich what you create with JavaScript if you like (I’ve got a few lines of JS that save those checkbox states to localStorage so they persist through page loads, for example).

But a CSS-based approach moves more of your functionality from the “nice to have” to “core” column. And that’s something we can all get behind, right?

CSS Lightbox with Alt Text Viewer

Rather I show you than tell you? Here’s a demo page.

Right now, my blog uses a CSS-based approach using anchors and the <dialog> element to make “lightbox” images work, but I plan to replace this soon with a similar approach that uses the <details> and <summary> elements to provide a more semantically-sound and accessible approach1.

An additional thing I wanted to implement – again, for the next version of my blog’s theme – was an “alt text viewer”. Mastodon has one, and it’s excellent2. Mastodon’s viewer requires JavaScript, but I was inspired when I saw James come up with a CSS-based version that used a re-styled checkbox.

But I wanted to do one better. Displaying alt text, too, seems like an example of what would semantically be best-represented by a <details>/<summary> pair.

Clicking on the image shows a larger version in a lightbox; clicking on the ‘alt’ button shows the alt text… all in semantic HTML and vanilla CSS.3

My first attempt tried to put the alt-text widget inside the <summary> of the original image, but that’s an accessibility no-no, so instead I wrap both <details> blocks (the lightbox, and the alt-text revealer) inside a container and then reposition the latter over the former. The rest is all the same kinds of tricks I demonstrated previously, to ensure that you can click in-and-out of both in an intuitive way and that keyboard navigation works as you’d expect.

I can’t use it on my blog yet (because if I do, it’ll probably break horribly when I add the functionality to my entire theme, later!), but I’ve put together a demonstration page that showcases the technique, plus a GitHub repo with all of the code (which is all public domain/unlicensed). Go have a play and tell me what you think!

Footnotes

1 As a secondary goal, using <details>/<summary> means that it’ll behave better when CSS is disabled or unavailable, which’ll make it easier to celebrate Naked CSS Day!

2 Why would I, a sighted person, need an alt text viewer, you ask? All kinds of reasons. Good alt text is for everybody, and can help by providing context, e.g. “explaining” the joke or identifying the probably-well-known-but-I-didn’t-recognise-them subject of a photo. Here’s some more reasons.

3 If you love owls and you love accessibility, this is the kind of example you should give a hoot about.

HTTP is not simple

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

HTTP/1 may appear simple because of several reasons: it is readable text, the most simple use case is not overly complicated and existing tools like curl and browsers help making HTTP easy to play with.

The HTTP idea and concept can perhaps still be considered simple and even somewhat ingenious, but the actual machinery is not.

[goes on to describe several specific characteristics of HTTP that make it un-simple, under the headings:

  • newlines
  • whitespace
  • end of body
  • parsing numbers
  • folding headers
  • never-implemented
  • so many headers
  • not all methods are alike
  • not all headers are alike
  • spineless browsers
  • size of the specs

]

I discovered this post late, while catching up on posts in the comp.infosystems.gemini newsgroup, but I’m glad I did because it’s excellent. Daniel Stenberg is, of course, the creator of cURL and so probably knows more about the intricacies of HTTP than virtually anybody (including, most-likely, some of the earliest contributors to its standards), and in this post he does a fantastic job of dissecting the oft-made argument that HTTP/1 is a “simple” protocol; based usually upon the argument that “if a human can speak it over telnet/netcat/etc., it’s simple”.

This argument, of course, glosses over the facts that (a) humans are not simple, and the things that we find “easy”… like reading a string of ASCII representations of digits and converting it into a representation of a number… are not necessarily easy for computers, and (b) the ways in which a human might use HTTP 0.9 through 1.1 are rarely representative of the complexities inherent in more-realistic “real world” use.

Obviously Daniel’s written about Gemini, too, and I agree with some of his points there (especially the fact that the specification intermingles the transfer protocol and the recommended markup language; ick!). There’s a reasonable rebuttal here (although it has its faults too, like how it conflates the volume of data involved in the encryption handshake with the processing overhead of repeated handshakes). But now we’re going way down the rabbithole and you didn’t come here to listen to me dissect arguments and counter-arguments about the complexities of Internet specifications that you might never use, right? (Although maybe you should: you could have been reading this blog post via Gemini, for instance…)

But if you’ve ever telnet’ted into a HTTP server and been surprised at how “simple” it was, or just have an interest in the HTTP specifications, Daniel’s post is worth a read.