Jim Nielsen’s Blog

You found my experimental HTML feed (there are also other ways to subscribe).

I HTML

Recent posts

CSS Space Toggles

View

I’ve been working on a transition to using light-dark() function in CSS.

What this boils down to is, rather than CSS that looks like this:

:root {
  color-scheme: light;
  --text: #000;
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --text: #fff;
  }
}

I now have this:

:root {
  color-scheme: light;
  --text: light-dark(#000, #fff);
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
  }
}

That probably doesn’t look that interesting. That’s what I thought when I first learned about light-dark() — “Oh hey, that’s cool, but it’s just different syntax. Six of one, half dozen of another kind of thing.”

But it does unlock some interesting ways to handling themeing which I will have to cover in another post. Suffice it to say, I think I’m starting to drink the light-dark() koolaid.

Anyhow, using the above pattern, I want to compose CSS variables to make a light/dark theme based on a configurable hue. Something like this:

:root {
  color-scheme: light;
  
  /* configurable via JS */
  --accent-hue: 56; 
  
  /* which then cascades to other derivations */
  --accent: light-dark(
    hsl(var(--accent-hue) 50% 100%),
    hsl(var(--accent-hue) 50% 0%),
  );
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
  }
}

The problem is that --accent-hue value doesn’t quite look right in dark mode. It needs more contrast. I need a slightly different hue for dark mode. So my thought is: I’ll put that value in a light-dark() function.

:root {
  --accent-hue: light-dark(56, 47);
  --my-color: light-dark(
    hsl(var(--accent-hue) 50% 100%),
    hsl(var(--accent-hue) 50% 0%),
  );
}

Unfortunately, that doesn’t work. You can’t put arbitrary values in light-dark(). It only accepts color values.

I asked what you could do instead and Roma Komarov told me about CSS “space toggles”. I’d never heard about these, so I looked them up.

First I found Chris Coyier’s article which made me feel good because even Chris admits he didn’t fully understand them.

Then Christopher Kirk-Nielsen linked me to his article which helped me understand this idea of “space toggles” even more.

I ended up following the pattern Christopher mentions in his article and it works like a charm in my implementation! The gist of the code works like this:

  1. When the user hasn’t specified a theme, default to “system” which is light by default, or dark if they’re on a device that supports prefers-color-scheme.
  2. When a user explicitly sets the color theme, set an attribute on the root element to denote that.
/* Default preferences when "unset" or "system" */
:root {
  --LIGHT: initial;
  --DARK: ;
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root {
    --LIGHT: ;
    --DARK: initial;
    color-scheme: dark;
  }
}

/* Handle explicit user overrides */
:root[data-theme-appearance="light"] {
  --LIGHT: initial;
  --DARK: ;
  color-scheme: light;
}
:root[data-theme-appearance="dark"] {
  --LIGHT: ;
  --DARK: initial;
  color-scheme: dark;
}

/* Now set my variables */
:root {
  /* Set the “space toggles’ */
  --accent-hue: var(--LIGHT, 56) var(--DARK, 47);
  
  /* Then use them */
  --my-color: light-dark(
    hsl(var(--accent-hue) 50% 90%),
    hsl(var(--accent-hue) 50% 10%),
  );
}

So what is the value of --accent-hue? That line sort of reads like this:

  • If --LIGHT has a value, return 56
  • else if --DARK has a value, return 47

And it works like a charm! Now I can set arbitrary values for things like accent color hue, saturation, and lightness, then leverage them elsewhere. And when the color scheme or accent color change, all these values recalculate and cascade through the entire website — cool!

A Note on Minification

A quick tip: if you’re minifying your HTML and you’re using this space toggle trick, beware of minifying your CSS! Stuff like this:

selector {
  --ON: ;
  --OFF: initial;
}

Could get minified to:

selector{--OFF:initial}

And this “space toggles trick” won’t work at all.

Trust me, I learned from experience.


Reply via: Email · Mastodon · Bluesky

Aspect Ratio Changes With CSS View Transitions

View

So here I am playing with CSS view transitions (again).

I’ve got Dave Rupert’s post open in one tab, which serves as my recurring reference for the question, “How do you get these things to work again?”

I’ve followed Dave’s instructions for transitioning the page generally and am now working on individual pieces of UI specifically.

I feel like I’m 98% of the way there, I’ve just hit a small bug.

It’s small. Many people might not even notice it. But I do and it’s bugging me.

When I transition from one page to the next, I expect this “active page” outline to transition nicely from the old page to the new one. But it doesn’t. Not quite.

Animated gif of a CSS page transition where the tab outline doesn’t grow proportionally but it happens really quickly so you barely see it.

Did you notice it? It’s subtle and fast, but it’s there. I have to slow my ::view-transition-old() animation timing waaaaay down to see catch it.

Animated gif of a CSS page transition where the tab outline doesn’t grow proportionally but it happens really slowly so you can definitely see it.

The outline grows proportionally in width but not in height as it transitions from one element to the next.

I kill myself on trying to figure out what this bug is.

Dave mentions in his post how he had to use fit-content to fix some issues with container changes between pages. I don’t fully understand what he’s getting at, but I think maybe that’s where my issue is? I try sticking fit-content on different things but none of it works.

I ask AI and it’s totally worthless, synthesizing disparate topics about CSS into a seemingly right on the surface but totally wrong answer.

So I sit and think about it.

What’s happening almost looks like some kind of screwy side effect of a transform: scale() operation. Perhaps it’s something about how default user agent styles for these things is animating the before/after state? No, that can’t be it…

Honestly, I have no idea. I don’t know much about CSS view transitions, but I know enough to know that I don’t know enough to even formulate the right set of keywords for a decent question. I feel stuck.

I consider reaching out on the socials for help, but at the last minute I somehow stumble on this perfectly wonderful blog post from Jake Archibald: “View transitions: Handling aspect ratio changes” and he’s got a one-line fix in my hands in seconds!

The article is beautiful. It not only gives me an answer, but it provides really wonderful visuals that help describe why the problem I’m seeing is a problem in the first place. It really helps fill out my understanding of how this feature works. I absolutely love finding writing like this on the web.

So now my problem is fixed — no more weirdness!

Animated gif of CSS multi-page transitions animating active tabs across pages of a website

If you’re playing with CSS view transitions these days, Jake’s article is a must read to help shape your understanding of how the feature works. Go give it a read.


Reply via: Email · Mastodon · Bluesky

Search Results Without JavaScript

View

I’m currently looking to add a search feature to my blog.

It’s a client-side approach, which means I was planning on using my favorite progressive-enhancement technique for client-side only search: you point a search form at Google, scope the results to your site, then use JavaScript to intercept the form submission and customize the experience on your site to your heart’s content.

<form action="https://www.google.com/search">
  <input type="text" name="q" placeholder="Search" />
  <input type="hidden" name="as_sitesearch" value="blog.jim-nielsen.com" />
  <button type="submit">Search</button>
</form>
<script>
    document.querySelector("form").addEventListener("submit", (e) => {
        e.preventDefault();
        // Do my client-side search stuff here
        // and stay on the current page
  });
</script>

However, then I remembered that Google Search no longer works without JavaScript which means this trick is no longer a trick. [1]

But have no fear, other search engines to the rescue!

DuckDuckGo, for example, supports this trick. Tweak some of the HTML from the Google example and it’ll work:

<form action="https://duckduckgo.com">
  <input type="text" name="q" placeholder="Search" />
  <input type="hidden" name="sites" value="blog.jim-nielsen.com" />
  <button type="submit">Search</button>
</form>
<script>
    document.querySelector("form").addEventListener("submit", (e) => {
        e.preventDefault();
        // Do my client-side search stuff here
        // and stay on the current page
  });
</script>

Yahoo also supports this trick, but not Bing. You can point people at Bing, but you can’t scope a query to your site only with an HTML form submission alone. Why? Because you need two search params: 1) a “query” param representing what the user typed into the search box, and 2) a “site search” param to denote which site you want to limit your results to (otherwise it’ll search the whole web).

From a UI perspective, if a search box is on your site, user intent is to search the content on your site. You don’t want to require people to type “my keywords site:blog.jim-nielsen.com” when they’re using a search box on your site — that’s just silly!

That’s why you need a second search parameter you can set yourself (a hidden input). You can’t concatenate something onto the end of a user’s HTML form submission. (What they type in the input box is what gets sent to the search engine as the ?q=... param.) To add to the q param, you would need JavaScript — but then that defeats the whole purpose of this exercise in the first place!

Anyhow, here are the search parameters I found useful for search engines that will support this trick:

  • DuckDuckGo:
    • Query: q
    • Site search param: sites
  • Yahoo
    • Query: p
    • Site search param: vs

I made myself a little test page for trying all these things. Check it out (and disable JS) if you want to try yourself!


  1. Not only that, but the as_sitesearch search param doesn’t seem to work anymore either. I can’t find any good documentation on what happened to as_sitesearch, but it seems like you’re supposed to use the “programmable search” now instead? Honestly I don’t know. And I don’t care enough to find out.

Reply via: Email · Mastodon · Bluesky

The Art of Making Websites

View

Hidde de Vries gave a great talked titled “Creativity cannot be computed” (you can checkout the slides or watch the video).

In his slides he has lots of bullet points that attempt to define what art is, and then in the talk he spends time covering each one. Here’s a sampling of the bullet points:

  • Art isn't always easy to recognize
  • Art has critics
  • Art is fuzzy
  • Art can make us think
  • Art can make the artist think
  • Art can make the audience think
  • Art can show us a mirror to reflect
  • Art can move us
  • Art can take a stance
  • Art can be used to show solidarity
  • Art can help us capture what it's like to be another person

I love all his bullet points. In fact, they got me thinking about websites.

I think you could substitute “website” for “art” in many of his slides. For example:

  • Art is repeated
  • Art may contain intentions
  • Art can show us futures we should not want
  • Art doesn’t have to fit in
    • You can make any kind of website. It gives you agency to respond to the world the way you want, not just by “liking” something on social media.
    • Me personally, I’ve made little websites meant to convey my opinion on social share imagery or reinforce the opinion I share with others on the danger of normalizing breakage on the web.
    • Each of those could’ve been me merely “liking” someone else’s opinion. Or I could’ve written a blog post. Or, as in those cases, I instead made a website.
  • Art can insult the audience
    • It doesn’t have to make you happy. Its purpose can be to offend you, or make you outraged and provoke a response. It can be a mother fucking website.

Of course, as Hidde points out, a website doesn’t have to be all of these. It also doesn’t have to be any of these.

Art — and a website — is as much about the artist and the audience as it is about the artifact. It’s a reflection of the person/people making it. Their intentions. Their purpose.

How’d you make it? Why’d you make it? When’d you make it? Each of these threads run through your art (website).

So when AI lets you make a website with the click of a button, it’s automating away a lot of the fun art stuff that goes into a website. The part where you have to wrestle with research, with your own intentions and motivations, with defining purpose, with (re)evaluating your world view.

Ultimately, a website isn’t just what you ship. It’s about who you are — and who you become — on the way to shipping.

So go explore who you are. Plumb the bottomless depths of self. Make art, a.k.a make a website.


Reply via: Email · Mastodon · Bluesky

Software Pliability

View

Quoting myself from former days on Twitter:

Businesses have a mental model of what they do.

Businesses build software to help them do it—a concrete manifestation of their mental model.

A gap always exists between these two.

What makes a great software business is their ability to keep that gap very small.

I think this holds up. And I still think about this idea (hence this post).

Software is an implementation of human understanding — people need X, so we made Y.

But people change. Businesses change. So software must also change.

One of your greatest strengths will be your ability to adapt and evolve your understanding of people’s needs and implement it in your software.

In a sense, technical debt is the other side of this coin of change: an inability to keep up with your own metamorphosis and understanding.

In a way, you could analogize this to the conundrum of rocket science: you need fuel to get to space, but the more fuel you add, the more weight you add, and the more weight you add, the more fuel you need. Ad nauseam.

It’s akin to making software.

You want to make great software for people’s needs today. It takes people, processes, and tools to make software, but the more people, processes, and tools you add to the machine of making software, the less agile you become. So to gain velocity you add more people, processes, and tools, which…you get the idea.

Being able to build and maintain pliable software that can change and evolve at the same speed as your mental model is a superpower. Quality in code means the flexibility to change.


Reply via: Email · Mastodon · Bluesky

Blown Away By the Unexpected

View

A friend gave me a copy of the book “Perfect Wave” by Dave Hickey.

I’ve been slowly reading through each essay and highlighting parts with my red pencil.

When I got to the chapter “Cool on Cool”, this passage stood out. I want to write it down and share it:

there was this perfect, luminous pop single by the Carpenters that just blew me away. And, believe me, the Carpenters were the farthest thing from my kind of thing. But when something that is not your thing blows you away, that’s one of the best things that can happen. It means you are something more and something other than you thought you were.

I find this beautiful.

I should take more time to wonder at moments of surprise I did not expect.

What a beautiful thing that I can be plowing through my existence and suddenly be surprised by something outside my taste, my beliefs, even my identity, that reaches in past all those things and rearranges me.

Perhaps my boundaries are more porous than I assume. In an instant, I can become something different, something more that I ever believed was possible.

Just think, that ability is lying there inside all of us.

I don’t have to think of myself as a walled garden but an open field. Who knows where my boundaries will expand to next. All it takes is someone walking by and tossing out a seed I would’ve never chosen to plant myself.

(Tangential: I love this interaction between Jerry Seinfeld and Brian Regan talking about being “blown away”.)


Reply via: Email · Mastodon · Bluesky

UI Pace Layers

View

Jeremy Keith, Chris Coyier, and others (see Jeremy’s post) have written about the idea of “pace layers” and now I’m going to take a stab at applying it to user interface primitives.

First, let’s start with a line of reasoning:

  • Common user interface controls — such as checkboxes or radios — should be visually and functionally consistent. This provides users a uniform, predictable interface for common interactions across various applications (turning things on/off, selecting an option from a pre-defined list, etc.).
  • Designers and developers should use the primitives afforded by the lower-levels they’re building on. This gives application and web site users a consistent, predictable interface within their context and environment.
  • For web designers, every person accessing your site has a particular piece of hardware with a particular operating system and design language to boot. You can build on top of those primitives, rather than re-create them yourself, which gives end users consistency within their chosen environment.
    • For example, a radio button on one website or in one native app is the same radio button on another website or in another app. (Rather than every single website and application rolling their own radios that might be identical, similar, or drastically different.)

To achieve this, user interfaces can be built in “layers” where each layer builds on top of the layer below it, providing a level of integration and consistency within its environment. And, where lower levels don’t provide primitives necessary for a user interface, designers and developers can create their own.

Diagram illustrating UI pace layers, with concentric layers representing the speed of change across different domains: websites (fast and iterative), design systems/brands, browsers, native apps, operating systems, and hardware (slow and stable).

In this world, individual websites are free to explore patterns and interactions which don’t yet exist (or are only half-implemented by lower layers). However, where a lower-level dependency exists, they can leverage it which gives end users a more consistent experience in their chosen environment while also giving designers and developers more time to focus on building UI controls and patterns that don’t yet exist.

UI Primitives We Build Ourselves Are a Liability

Every UI control you roll yourself is a liability. You have to design it, test it, ship it, document it, debug it, maintain it — the list goes on.

It makes you wonder why we insist on rolling (or styling) our own common UI controls so often. Perhaps we’d be better off asking: What are the fewest amount of components we have to build to deliver value to our users?

When creating user interfaces, you can leverage the existing controls and patterns of the layers you’re building on top of.

This helps you build, maintain, and debug less because you’re using primitives built and maintained by the makers of levels below you (which are generally more stable and change less).

And it helps your users because the experience of your app or website is more consistent and predictable within whatever particular hardware and operating system they’re using.

Adopting UI Primitives: Addition or Subtraction

When designers and developers set out to create a user interface, they have to ask themselves: Are we going to use something that already exists, or create our own?

When you use a checkbox or radio control, are you adopting those controls by 1) leveraging existing APIs of lower-level layers, or 2) re-implementing them yourself?

Approach (1) makes it easier for you to maintain and easier for your users to use, as it’s a pattern consistent with the shared language and functionality of their bespoke computing experience.

Approach (2) does neither of these. It’s more work for you to build and maintain, and it’s more cognitive work for your users to learn yet another visual and functional variant of an otherwise standard UI primitive.

An Example: The Switch Control

For a long time, a checkbox was all you had on the web. So people built their own “switch” controls.

Eventually, browsers got around to providing an API to the existing switch control of lower-level systems.

So the question for many websites and design systems became: Do we adopt the switch control that the browser (and lower-level layers) now provide us? Or do we keep our hand-rolled switch?

In this sense, there are two approaches to building a design system:

  1. Build everything that’s needed.
  2. Build only what is not already provided by lower layers (and trade variance in your system for consistency in your users’ systems).

In approach (1) you build and maintain everything yourself. In approach (2) you build what isn’t provided and you maintain by deleting previous implementations now provided by lower layers.

Priority Says: Brands > People

In a world of layers built on top of each other, you would see updates to UI primitives change in lower levels and “bubble up” to websites.

OS -> Native -> Browser -> Website -> Form control

However, the world we’ve constructed with many of our websites and design systems is outside of this flow of updates. There’s the OS-level stuff:

OS -> Native -> Browser

And then tangential to that stream of updates is this flow, which requires manual intervention and updates:

Design system -> Website -> Form control

For example, if the OS changes its radios, websites only get those updates if individual design system and website owners decide to pick up those changes, leaving users’ experiences inconsistent in their chosen computing environment.

In other words, the UI layering of operating systems and websites diverge from each other. We opt to make the experience of our brands primary over the users’.

Whereas we could be choosing to make our brands fit into users' choices.

But then we’d have to value honoring user choice over brand consistency, and I just don’t know if the world is ready for that because brands pay the bills.


Reply via: Email · Mastodon · Bluesky

Notes on Google Search Now Requiring JavaScript

View

John Gruber has a post about how Google’s search results now require JavaScript[1]. Why? Here’s Google:

the change is intended to “better protect” Google Search against malicious activity, such as bots and spam

Lol, the irony.

Let’s turn to JavaScript for protection, as if the entire ad-based tracking/analytics world born out of JavaScript’s capabilities isn’t precisely what led to a less secure, less private, more exploited web.

But whatever, “the web” is Google’s product so they can do what they want with it — right? Here’s John:

Old original Google was a company of and for the open web. Post 2010-or-so Google is a company that sees the web as a de facto proprietary platform that it owns and controls. Those who experience the web through Google Chrome and Google Search are on that proprietary not-closed-per-se-but-not-really-open web.

Search that requires JavaScript won’t cause the web to die. But it’s a sign of what’s to come (emphasis mine):

Requiring JavaScript for Google Search is not about the fact that 99.9 percent of humans surfing the web have JavaScript enabled in their browsers. It’s about taking advantage of that fact to tightly control client access to Google Search results. But the nature of the true open web is that the server sticks to the specs for the HTTP protocol and the HTML content format, and clients are free to interpret that as they see fit. Original, novel, clever ways to do things with website output is what made the web so thrilling, fun, useful, and amazing. This JavaScript mandate is Google’s attempt at asserting that it will only serve search results to exactly the client software that it sees fit to serve.

Requiring JavaScript is all about control.

The web was founded on the idea of open access for all. But since that’s been completely and utterly abused (see LLM training datasets) we’re gonna lose it.

The whole “freemium with ads” model that underpins the web was exploited for profit by AI at an industrial scale and that’s causing the “free and open web” to become the “paid and private web”.

Universal access is quickly becoming select access — Google search results included.


  1. If you want to go down a rabbit hole of reading more about this, there’s the TechCrunch article John cites, a Hacker News thread, and this post from a company founded on providing search APIs.

Reply via: Email · Mastodon · Bluesky

Related posts linking here: (2025) Search Results Without JavaScript

Missed Connections

View

Let me tell you about one of the best feelings.

You have a problem.

You bang your head on it for a while.

Through the banging, you formulate a string of keywords describing the problem.

You put those words into a search engine.

You land on a forum or a blog post and read someone else’s words containing those keywords and more. Their words resonate with you deeply.

They’re saying the exact same things you were saying to yourself in your head.

You immediately know, “This person gets it!”

You know they have an answer to your problem. They’ve seen what you’re seeing.

And on top of it all, they provide a solution which fixes your problem!

A sense of connection is now formed. You feel validated, understood, seen. They’ve been through what you’re going through, and they wrote about it to reach out to you — across time and space.

I fell in love with the web for this reason, this feeling of connection. You could search the world and find someone who saw what you see, felt what you feel, went through what you’re going through.

Contrast that with today.

Today you have a problem.

You bang your head on it.

You ask a question in a prompt.

And you get back something.

But there’s no human behind it. Just a machine which takes human voices and de-personalizes them until the individual point of view is annihilated. And so too with it the sense of connection — the feeling of being validated, understood, seen.

Every prompt a connection that could have been. A world of missed connections.


Reply via: Email · Mastodon · Bluesky

Related posts linking here: (2025) Aspect Ratio Changes With CSS View Transitions

HTML Minification for Static Sites

View

This is a note to my future self, as I’ve setup HTML minification on a few different projects and each time I ask myself, “How did I do that again?” So here’s your guide, future Jim (and anyone else on the internet who finds this).

I use html-minifier to minifiy HTML files created by my static site generator. Personally, I use the CLI tool because it's easy to add a CLI command as an npm postbuild step.

Example package.json:

{
  "scripts": {
    "build": "<BUILD-COMMAND>"
    "postbuild": "html-minifier --input-dir <BUILD-DIR> --output-dir <BUILD-DIR> --file-ext html <OPTIONS>"
  }
}

All the minification options are off by default, so you have to turn them on one-by-one (HTML minfication is a tricky concern). Me personally, I’m using the ones exemplified in the project README:

--collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true

So, for a site folder named build, the entire command looks like this:

html-minifier --input-dir ./build --output-dir ./build --file-ext html --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true

That’s it — that’s the template.

What Kind of Results Do I Get?

I use this on a few of my sites, including my notes site and this blog.

When testing it locally for my blog’s build, I:

  • Run a build and put files to ./build
  • Copy ./build to ./build-min
    • Command: cp -R build build-min
  • Run html-minifier on build-min and compare the resulting folders in macOS finder.

Here’s my results for my blog (2,501 items in ./build):

  • Directory size:
    • Before: 37MB
    • After: 28.4MB
    • Difference: ▼ -8.6MB (-23.24%)
  • Main index.html file lines of code:
    • Before: 1,484
    • After: 15 lines
    • Difference: ▼ -1,469 lines (-99%)
  • Main index.html file size over the network:
    • Before: 30.6kB
    • After: 17.6kB
    • Difference: ▼ -13kB (-42.48%)

And the results for my notes (one big index.html file):

  • File size:
    • Before: 1.5MB
    • After: 1.1MB
    • Difference: ▼ -0.4MB (-26.67%)
  • Lines of code:
    • Before: 25,974
    • After: 1
    • Difference: ▼ -25,973 lines (-99.996%)

Reply via: Email · Mastodon · Bluesky

Related posts linking here: (2025) CSS Space Toggles