Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
72
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...
29th Jan 2025

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Jim Nielsen’s Blog

Browser APIs: The Web’s Free SaaS

Authentication on the web is a complicated problem. If you’re going to do it yourself, there’s a lot you have to take into consideration. But odds are, you’re building an app whose core offering has nothing to do with auth. You don’t care about auth. It’s an implementation detail. So rather than spend your precious time solving the problem of auth, you pay someone else to solve it. That’s the value of SaaS. What would be the point of paying for an authentication service, like workOS, then re-implementing auth on your own? They have dedicated teams working on that problem. It’s unlikely you’re going to do it better than them and still deliver on the product you’re building. There’s a parallel here, I think, to building stuff in the browser. Browsers provide lots of features to help you deliver good websites fast to an incredibly broad and diverse audience. Browser makers have teams of people who, day-in and day-out, are spending lots of time developing and optimizing new their offerings. So if you leverage what they offer you, that gives you an advantage because you don’t have to build it yourself. You could build it yourself. You could say “No thanks, I don’t want what you have. I’ll make my own.” But you don’t have to. And odds are, whatever you do build yourself, is not likely to be as fast as the highly-optimized subsystems you can tie together in the browser. And the best part? Unlike SasS, you don’t have to pay for what the browser offers you. And because you’re not paying, it can’t be turned off if you stop paying. @view-transition, for example, is a free API that’ll work forever. That’s a great deal. Are you taking advantage? Email · Mastodon · Bluesky

2nd Nov 2025 14 votes
Doing It Manually

I have a standing desk that goes up and down via a manual crank. I’ve had it for probably ten years. Every time I raise or lower that thing, it gets my blood pumping. I often think: “I should upgrade to one of those standing desks that goes up and down with the push of a button.” Then there’s the other voice in my head: “Really? Are you so lazy you can’t put your snacks down, get out of your comfy chair, in your air conditioned room, and raise or lower your desk using a little elbow grease? That desk is just fine.” While writing this, I get out of my chair, star the timer, and raise my desk to standing position. 35 seconds. That’s the cost: 35 seconds, and an elevated heart rate. As I have many times over the last ten years, I recommit to keeping it — mostly as a reminder that it’s ok to do some things manually. Not everything in my life needs to be available to me at the push of a button. Email · Mastodon · Bluesky

2nd Oct 2025 86 votes
Trying to Make Sense of Casing Conventions on the Web

(I present to you my stream of consciousness on the topic of casing as it applies to the web platform.) I’m reading about the new command and commandfor attributes — which I’m super excited about, declarative behavior invocation in HTML? YES PLEASE!! — and one thing that strikes me is the casing in these APIs. For example, the command attribute has a variety of values in HTML which correspond to APIs in JavaScript. The show-popover attribute value maps to .showPopover() in JavaScript. hide-popover maps to .hidePopover(), etc. So what we have is: lowercase in attribute names e.g. commandfor="..." kebab-case in attribute values e.g. show-popover camelCase for JS counterparts e.g. showPopover() After thinking about this a little more, I remember that HTML attributes names are case insensitive, so the browser will normalize them to lowercase during parsing. Given that, I suppose you could write commandFor="..." but it’s effectively the same. Ok, lowercase attribute names in HTML makes sense. The related popover attributes follow the same convention: popovertarget popovertargetaction And there are many other attribute names in HTML that are lowercase, e.g.: maxlength novalidate contenteditable autocomplete formenctype So that all makes sense. But wait, there are some attribute names with hyphens in them, like aria-label="..." and data-value="...". So why isn’t it command-for="..."? Well, upon further reflection, I suppose those attributes were named that way for extensibility’s sake: they are essentially wildcard attributes that represent a family of attributes that are all under the same namespace: aria-* and data-*. But wait, isn’t that an argument for doing popover-target and popover-target-action? Or command and command-for? But wait (I keep saying that) there are kebab-case attribute names in HTML — like http-equiv on the <meta> tag, or accept-charset on the form tag — but those seem more like legacy exceptions. It seems like the only answer here is: there is no rule. Naming is driven by convention and decisions are made on a case-by-case basis. But if I had to summarize, it would probably be that the default casing for new APIs tends to follow the rules I outlined at the start (and what’s reflected in the new command APIs): lowercase for HTML attributes names kebab-case for HTML attribute values camelCase for JS counterparts Let’s not even get into SVG attribute names We need one of those “bless this mess” signs that we can hang over the World Wide Web. Email · Mastodon · Bluesky

4th Sep 2025 42 votes
A Few Things About the Anchor Element’s href You Might Not Have Known

I’ve written previously about reloading a document using only HTML but that got me thinking: What are all the values you can put in an anchor tag’s href attribute? Well, I looked around. I found some things I already knew about, e.g. Link protocols like mailto:, tel:, sms: and javascript: which deal with specific ways of handling links. Protocol-relative links, e.g. href="//" Text fragments for linking to specific pieces of text on a page, e.g. href="#:~:text=foo" But I also found some things I didn’t know about (or only vaguely knew about) so I wrote them down in an attempt to remember them. href="#" Scrolls to the top of a document. I knew that. But I’m writing because #top will also scroll to the top if there isn’t another element with id="top" in the document. I didn’t know that. (Spec: “If decodedFragment is an ASCII case-insensitive match for the string top, then return the top of the document.”) href="" Reloads the current page, preserving the search string but removing the hash string (if present). URL href="" resolves to /path/ /path/ /path/#foo /path/ /path/?id=foo /path/?id=foo /path/?id=foo#bar /path/?id=foo href="." Reloads the current page, removing both the search and hash strings (if present). Note: If you’re using href="." as a link to the current page, ensure your URLs have a trailing slash or you may get surprising navigation behavior. The path is interpreted as a file, so "." resolves to the parent directory of the current location. URL href="." resolves to /path / /path#foo / /path?id=foo / /path/ /path/ /path/#foo /path/ /path/?id=foo /path/ /path/index.html /path/ href="?" Reloads the current page, removing both the search and hash strings (if present). However, it preserves the ? character. Note: Unlike href=".", trailing slashes don’t matter. The search parameters will be removed but the path will be preserved as-is. URL href="?" resolves to /path /path? /path#foo /path? /path?id=foo /path? /path?id=foo#bar /path? /index.html /index.html? href="data:" You can make links that navigate to data URLs. The super-readable version of this would be: <a href="data:text/plain,hello world"> View plain text data URL </a> But you probably want data: URLs to be encoded so you don’t get unexpected behavior, e.g. <a href="data:text/plain,hello%20world"> View plain text data URL </a> Go ahead and try it (FYI: may not work in your user agent). Here’s a plain-text file and an HTML file. href="video.mp4#t=10,20" Media fragments allow linking to specific parts of a media file, like audio or video. For example, video.mp4#t=10,20 links to a video. It starts play at 10 seconds, and stops it at 20 seconds. (Support is limited at the time of this writing.) See For Yourself I tested a lot of this stuff in the browser and via JS. I think I got all these right. Thanks to JavaScript’s URL constructor (and the ability to pass a base URL), I could programmatically explore how a lot of these href’s would resolve. Here’s a snippet of the test code I wrote. You can copy/paste this in your console and they should all pass 🤞 const assertions = [ // Preserves search string but strips hash // x -> { search: '?...', hash: '' } { href: '', location: '/path', resolves_to: '/path' }, { href: '', location: '/path/', resolves_to: '/path/' }, { href: '', location: '/path/#foo', resolves_to: '/path/' }, { href: '', location: '/path/?id=foo', resolves_to: '/path/?id=foo' }, { href: '', location: '/path/?id=foo#bar', resolves_to: '/path/?id=foo' }, // Strips search and hash strings // x -> { search: '', hash: '' } { href: '.', location: '/path', resolves_to: '/' }, { href: '.', location: `/path#foo`, resolves_to: `/` }, { href: '.', location: `/path?id=foo`, resolves_to: `/` }, { href: '.', location: `/path/`, resolves_to: `/path/` }, { href: '.', location: `/path/#foo`, resolves_to: `/path/` }, { href: '.', location: `/path/?id=foo`, resolves_to: `/path/` }, { href: '.', location: `/path/index.html`, resolves_to: `/path/` }, // Strips search parameters and hash string, // but preserves search delimeter (`?`) // x -> { search: '?', hash: '' } { href: '?', location: '/path', resolves_to: '/path?' }, { href: '?', location: '/path#foo', resolves_to: '/path?' }, { href: '?', location: '/path?id=foo', resolves_to: '/path?' }, { href: '?', location: '/path/', resolves_to: '/path/?' }, { href: '?', location: '/path/?id=foo#bar', resolves_to: '/path/?' }, { href: '?', location: '/index.html#foo', resolves_to: '/index.html?'} ]; const assertions_evaluated = assertions.map(({ href, location, resolves_to }) => { const domain = 'https://example.com'; const expected = new URL(href, domain + location).toString(); const received = new URL(domain + resolves_to).toString(); return { href, location, expected: expected.replace(domain, ''), received: received.replace(domain, ''), passed: expected === received }; }); console.table(assertions_evaluated); Email · Mastodon · Bluesky

3rd Aug 2025 41 votes
Setting Element Ordering With HTML Rewriter Using CSS

After shipping my work transforming HTML with Netlify’s edge functions I realized I have a little bug: the order of the icons specified in the URL doesn’t match the order in which they are displayed on screen. Why’s this happening? I have a bunch of links in my HTML document, like this: <icon-list> <a href="/1/">…</a> <a href="/2/">…</a> <a href="/3/">…</a> <!-- 2000+ more --> </icon-list> I use html-rewriter in my edge function to strip out the HTML for icons not specified in the URL. So for a request to: /lookup?id=1&id=2 My HTML will be transformed like so: <icon-list> <!-- Parser keeps these two --> <a href="/1/">…</a> <a href="/2/">…</a> <!-- But removes this one --> <a href="/3/">…</a> </icon-list> Resulting in less HTML over the wire to the client. But what about the order of the IDs in the URL? What if the request is to: /lookup?id=2&id=1 Instead of: /lookup?id=1&id=2 In the source HTML document containing all the icons, they’re marked up in reverse chronological order. But the request for this page may specify a different order for icons in the URL. So how do I rewrite the HTML to match the URL’s ordering? The problem is that html-rewriter doesn’t give me a fully-parsed DOM to work with. I can’t do things like “move this node to the top” or “move this node to position x”. With html-rewriter, you only “see” each element as it streams past. Once it passes by, your chance at modifying it is gone. (It seems that’s just the way these edge function tools are designed to work, keeps them lean and performant and I can’t shoot myself in the foot). So how do I change the icon’s display order to match what’s in the URL if I can’t modify the order of the elements in the HTML? CSS to the rescue! Because my markup is just a bunch of <a> tags inside a custom element and I’m using CSS grid for layout, I can use the order property in CSS! All the IDs are in the URL, and their position as parameters has meaning, so I assign their ordering to each element as it passes by html-rewriter. Here’s some pseudo code: // Get all the IDs in the URL const ids = url.searchParams.getAll("id"); // Select all the icons in the HTML rewriter.on("icon-list a", { element: (element) => { // Get the ID const id = element.getAttribute('id'); // If it's in our list, set it's order // position from the URL if (ids.includes(id)) { const order = ids.indexOf(id); element.setAttribute( "style", `order: ${order}` ); // Otherwise, remove it } else { element.remove(); } }, }); Boom! I didn’t have to change the order in the source HTML document, but I can still get the displaying ordering to match what’s in the URL. I love shifty little workarounds like this! Email · Mastodon · Bluesky

2nd Jul 2025 33 votes

More in design

The least wrong colors, version 2

Four years ago, I wrote “How to pick the least wrong colors.” The gist is: picking a categorical color palette is an optimization problem. There’s no such thing as the right colors. But if you use the right cost function, and the right kind of hill climbing, you can at least get the least wrong ones. Since the original post I’ve been slowly picking away at improvements and new approaches. Now that we’re past the singularity, I’ve put a few coding robots on the job. It’s reassuring that many of my assumptions were good ones! The robots have been able to improve the code, bridging some of the gaps in my own knowledge. Today, I’m publishing an updated version of the algorithm as an npm package, along with a fancy GUI version. While there’s still more to do, I’m proud of how far I’ve been able to take it. What’s new New evaluators More controls The public API and a CLI What’s improved The annealing algorithm Configurable color space and distance metric The results One more thing Acknowledgements What’s new New evaluators Almost as soon as I published the first version, I realized that the cost function lends itself really well to modularity. Beyond my initial evaluation functions, I could design new ones, and provide a framework for anyone to plug in their own. As a recap, my original criteria for good categorical colors, mapped to evaluation functions: Similarity — a way of measuring the similarity of one palette to another, useful for providing art direction and getting brand alignment Energy — the colors should be different from each other so they aren’t liable to be confused from one another Range — the differences between the colors should be consistent so unintended groupings don’t appear Color vision deficiency — simulating the colors under different types of color blindness (red-green, blue-yellow, partial to full tritanopia) Here’s the new evaluators: JND — strongly reject palettes that have two or more colors that are too similar Avoid — the mirror image of the similarity evaluation, push colors away from a user-defined set Contrast — compares colors, keeping them above the WCAG AA color contrast floor. Can be used with a background color to maintain contrast on a chart’s background Saliency — uses color naming study data to prefer colors that are easy to name Name difference — the mirror image of saliency, avoiding colors that share names Each of these evaluators can be weighted, indicating the kinds of tradeoffs and priorities you’d like for your color palette. Additionally, the whole evaluator system is pluggable: you can define your own evaluators and have them drive the optimizer! More controls Colors can now be fixed in place, or pinned to a particular order, making it easier to load in existing palettes and optimize all or just some of the colors. Individual channels of each color can be locked, too, meaning you can keep the saturation or hue of a color fixed while optimizing its lightness. This works in any color space. The public API and a CLI The whole package is now a proper library, with a public API. This means: 1. the whole thing is now distributable through npm, with proper versioning, 2. there’s a CLI, making it much more ergonomic for both humans and agents. The API allows for full configuration of the algorithm, as well as loading in colors to optimize. Output can be in raw color values, CSS properties, or DTCG JSON. There’s also a new reportJndIssues endpoint that allows you to evaluate palettes without optimizing them, which is useful to compare a generated palette to commonly-used ones (like Observable, d3, IBM Carbon, and more). What’s improved The annealing algorithm When I wrote the initial algorithm in 2022, I had just learned about simulated annealing. I’ll be honest: I don’t know much more today than I did then. But with AI-assisted research, I was able to solve some questions I had about the initial implementation. Now, the algorithm picks the correct starting temperature based on some random initial samples. Mutation also happens in a scaled manner, so colors change less towards the end of the optimization schedule. Iterations can be capped to prevent very long runs, and the whole thing is much, much more performant. Configurable color space and distance metric The first version of the algorithm worked in RGB space. Now, it defaults to okhsl, but even this is configurable. Individual channels can be constrained to dial in the palette’s boundaries. Also, you can choose which color distance metric you’d like to use (but the library uses CIEDE2000 by default). This flexibility is powered largely by a move from chroma.js to culori. I’ve learned a ton about color spaces since 2022, so being able to mix and match color spaces with distance metrics has been extremely useful. The results The category-colors library reliably produces better results than other palette-generating tools and industry-standard color palettes. Compared to other palette-generating tools, category-colors has more control. Palettailor, for example, optimizes for pure color difference, without accounting for color vision deficiency. QualPal brings some of the optimization parameters, but doesn’t allow for steering towards or away from arbitrary colors. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 13.7 ±2.0 0.35 ±0.14 best in column 0.30 ±0.02 best in column QualPal 1.1.0 24.7 21.8 best in column 0.10 0.44 Palettailor 26.6 ±2.4 best in column 4.5 ±1.7 0.34 ±0.16 0.34 ±0.04 Colorgorical 15.8 ±3.3 4.1 ±1.6 0.09 ±0.06 0.42 ±0.03 All numbers are at 8 colors. Rows with ± are mean ± standard deviation over 10 palettes; rows without are deterministic and produce one palette. category-colors and Palettailor are 10 independent runs on the same seeds; Colorgorical’s row is 10 palettes from its authors’ own sampling script at equal criterion weights. QualPal was run with CVD on, matched bounds, and takes no seed. Name difference is Heer & Stone’s 1 − cosine; Colorgorical’s own interface reports a Hellinger distance instead. Shaded cells are the best value in their column. Compared to industry-standard palettes, category-colors can produce more optimal palettes, especially at high cardinality. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 best in column 13.7 ±2.0 best in column 0.35 ±0.14 0.30 ±0.02 best in column Okabe–Ito 21.3 8.8 0.06 0.34 Observable 10 18.4 0.6 0.40 0.34 Tableau 10 18.1 3.2 0.24 0.32 d3 category10 16.2 1.6 0.84 best in column 0.40 ColorBrewer Set3 13.7 1.9 0.16 0.32 IBM Carbon 12.8 5.0 0.11 0.34 Same run: 8 colors, 10 trials. Reference palettes are deterministic, so they're single values. Shaded cells are the best value in their column. One more thing I’ve built a UI that consumes the package and makes it easy to generate and optimize palettes. This has been the biggest request since I published the initial essay, so it’s the thing I’m excited to share. It’s ridiculously overengineered, but hey, what else are personal projects for? Acknowledgements Many measurements come from published research: Gaurav Sharma, Wencheng Wu and Edul Dalal for CIEDE2000; Gustavo Machado, Manuel Oliveira and Leandro Fernandes for the color vision deficiency simulation; Maureen Stone, Danielle Albers Szafir and Vidya Setlur for the size-dependent just-noticeable-difference result; Jeffrey Heer and Maureen Stone, whose color naming models and the c3 data from the Stanford Visualization Group power both the saliency and name-difference evaluators. Existing palettes: Masataka Okabe and Kei Ito’s Color Universal Design set; Matthew Petroff’s sequences; and Mark Harrower and Cynthia Brewer’s ColorBrewer. Other generators laid a lot of the groundwork: Kecheng Lu and colleagues (Palettailor), Connor Gramazio, David Laidlaw and Karen Schloss (Colorgorical), Johan Larsson (QualPal), and Chin Tseng, Arran Zeyu Wang, Ghulam Jilani Quadri and Danielle Albers Szafir (CatPAW). Andrew McNutt, Maureen Stone and Jeffrey Heer’s color-buddy has also been indispensable. Finally, Dan Burzo’s culori made it easy to make this library colorspace-agnostic.

a week ago 1 votes
Mountains of work

This is part of a new experiment I started in an effort to document the process of making Niche design.

a week ago 1 votes
When the canvas starts acting, who’s really in control?

Weekly curated resources for designers — thinkers and makers.

a week ago 1 votes
Gestalt Principles for Visual UI Design

Users parse a layout before they read its labels. Whitespace, borders, alignment, color, and motion determine what belongs together. When these cues fight the content, users attach the label, price, warning, status, or action to the wrong object. Proximity, similarity, enclosure, and the other Gestalt cues guide the eye, snapping visual chaos into clarity.

2 weeks ago 2 votes
The silks of Como: A reminder of the value of craft

I miss this. Visiting a mill is such an enjoyable, often inspiring experience, yet I don’t do it as much as I used to. Perhaps because once you’d done one worsted weaver it’s hard to justify more.  But we’ve never done silk. I did go to Vann... > Read more

2 weeks ago 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in