Jim Nielsen’s Blog

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

I HTML

Recent posts

Making Icon Sets Easy With Web Origami

View

Over the years, I’ve used different icon sets on my blog. Right now I use Heroicons.

The recommended way to use them is to copy/paste the source from the website directly into your HTML. It’s a pretty straightforward process:

  • Go to the website
  • Search for the icon you want
  • Hover it
  • Click to “Copy SVG”
  • Go back to your IDE and paste it

If you’re using React or Vue, there are also npm packages you can install so you can import the icons as components.

But I’m not using either of those frameworks, so I need the raw SVGs and there’s no npm i for those so I have to manually grab the ones I want.

In the past, my approach has been to copy the SVGs into individual files in my project, like:

src/
  icons/
    home.svg
    about.svg
    search.svg

Then I have a “component” for reading those icons from disk which I use in my template files to inline the SVGs in my HTML. For example:

// Some page template file
import { Icon } from './Icon.js'
const template = `<div>${Icon('search.svg')} Search</div>`

// Icon.js
import fs from 'fs'
import path from 'path'
const __dirname = /* Do the stuff to properly resolve the file path */;
export const Icon = (name) => fs.readFileSync(
  path.join(__dirname, 'icons', name),
  'utf8'
).toString();

It’s fine. It works. It’s a lot of node boilerplate to read files from disk.

But changing icons is a bit of a pain. I have to find new SVGs, overwrite my existing ones, re-commit them to source control, etc.

I suppose it would be nice if I could just npm i heroicons and get the raw SVGs installed into my node_modules folder and then I could read those. But that has its own set of trade-offs. For example:

  • Names are different between icon packs, so when you switch, names don’t match. For example, an icon might be named search in one pack and magnifying-glass in another. So changing sets requires going through all your templates and updating references.
  • Icon packs are often quite large and you only need a subset. npm i icon-pack might install hundreds or even thousands of icons I don’t need.

So the project’s npm packages don’t provide the raw SVGs. The website does, but I want a more programatic way to easily grab the icons I want.

How can I do this?

Enter Origami

I’m using Web Origami for my blog which makes it easy to map icons I use in my templates to Heroicons hosted on Github. It doesn’t require an npm install or a git submodule add. Here’s an snippet of my file:

{
  home.svg: https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/heads/master/optimized/24/outline/home.svg,
  about.svg: https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/heads/master/optimized/24/outline/question-mark-circle.svg,
  search.svg: https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/heads/master/optimized/24/outline/magnifying-glass.svg
}

As you can see, I name my icon (e.g. search) and then I point it to the SVG as hosted on Github via the Heroicons repo. Origami takes care of fetching the icons over the network and caching them in-memory.

Beautiful, isn’t it? It kind of reminds me of import maps where you can map a bare module specifier to a URL (and Deno’s semi-abandoned HTTP imports which were beautiful in their own right).

How It Works

Origami makes file paths first-class citizens of the language — even “remote” file paths — so it’s very simple to create a single file that maps your icon names in a codebase to someone else’s icon names from a set, whether those are being installed on disk via npm or fetched over the internet.

To simplify my example earlier, I can have a file like icons.ori:

{
  home.svg: https://example.com/path/to/home.svg
  about.svg: https://example.com/path/to/information-circle.svg
  search.svg: https://example.com/path/to/magnifying-glass.svg
}

Then I can reference those icons in my templates like this:

<div>${icons.ori/home.svg} Search</div>

Easy-peasy! And when I want to change icons, I simply update the entries in icons.ori to point somewhere else — at a remote or local path.

And if you really want to go the extra mile, you can use Origami’s caching feature:

Tree.cache(
  {
    home.svg: https://raw.github.com/path/to/home.svg
    about.svg: https://raw.github.com/path/to/information-circle.svg
    search.svg: https://raw.github.com/path/to/magnifying-glass.svg
  },
  Origami.projectRoot()/cache
)

Rather than just caching the files in memory, this will cache them to a local folder like this:

cache/
  home.svg
  about.svg
  search.svg

Which is really cool because now when I run my site locally I have a folder of SVG files cached locally that I can look at and explore (useful for debugging, etc.)

This makes vendoring really easy if I want to put these in my project under source control. Just run the file once and boom, they’re on disk!

There’s something really appealing to me about this. I think it’s because it feels very “webby” — akin to the same reasons I liked HTTP imports in Deno. You declare your dependencies with URLs, then they’re fetched over the network and become available to the rest of your code. No package manager middleman introducing extra complexity like versioning, transitive dependencies, install bloat, etc.

What’s cool about Origami is that handling icons like this isn’t a “feature” of the language. It’s an outcome of the expressiveness of the language. In some frameworks, this kind of problem would require a special feature (that’s why you have special npm packages for implementations of Heroicons in frameworks like react and vue). But because of the way Origami is crafted as a tool, it sort of pushes you towards crafting solutions in the same manner as you would with web-based technologies (HTML/CSS/JS). It helps you speak “web platform” rather than some other abstraction on top of it. I like that.


Reply via: Email · Mastodon · Bluesky

How AI Labs Proliferate

View

SITUATION: there are 14 competing AI labs.

“We can’t trust any of these people with super-intelligence. We need to build it ourselves to ensure it’s done right!"

“YEAH!”

SOON: there are 15 competing AI labs.

(See: xkcd on standards.)


The irony: “we’re the responsible ones” is each lab’s founding mythology as they spin out of each other.


Reply via: Email · Mastodon · Bluesky

A Few Rambling Observations on Care

View

In this new AI world, “taste” is the thing everyone claims is the new supreme skill.

But I think “care” is the one I want to see in the products I buy.


Can you measure care?

Does scale drive out care?

If a product conversation is reduced to being arbitrated exclusively by numbers, is care lost?

The more I think about it, care seems antithetical to the reductive nature of quantification — “one death is a tragedy, one million is a statistic”.


Care considers useful, constructive systematic forces — rules, processes, etc. — but does not take them as law. Individual context and sensitivity are the primary considerations.

That’s why the professional answer to so many questions is: “it depends”.

“This is the law for everyone, everywhere, always” is not a system I want to live in.


Businesses exist to make money, so one would assume a business will always act in a way that maximizes the amount of money that can be made.

That’s where numbers take you. They let you measure who is gaining or losing the most quantifiable amount in any given transaction.

But there’s an unmeasurable, unquantifiable principle lurking behind all those numbers: it can be good for business to leave money on the table.

Why? Because you care. You are willing to provision room for something beyond just a quantity, a number, a dollar amount.


I don’t think numbers alone can bring you to care.

I mean, how silly is it to say:

“How much care did you put into the product this week?”

“Put me down for a 8 out of 10 this week.”


Reply via: Email · Mastodon · Bluesky

Unresponsive Buttons on My Fastest Hardware Ever

View

This is one of those small things that drives me nuts.

Why? I don’t know. I think it has something to do with the fact that I have a computer that is faster than any computer I’ve ever used in my entire life — and yet, clicking on buttons results in slight but perceptible delays.

Let me explain.

Imagine a button that looks like this:

<Button
  onClick={async () => {
    const data = await getSessionUrlFromStripe(id);
    window.location = data.url;
  }
>Upgrade to Pro</Button>

For SPA apps, when the user clicks that button it takes a split second (even on a fast connection) for anything to happen because:

  • The browser makes a request to the server
  • The server talks to Stripe to get a session
  • The server responds with the session data to the client
  • The client redirects

When clicking on that button, even on a fast connection, my brain glitches for a second, my thought process going something like:

  • I click
  • [nothing happens]
  • I think “Did that work?”
  • Just as I’m about to click again, I see the URL bar change
  • I think, “Oh, ok, it’s doing something.”
  • I stop myself from clicking again while I wait for the UI to redraw

Granted those thoughts occur in my brain in under a second, but I hate that pause of indetermination.

I clicked, I want (perceptibly) instant feedback. If something is happening, tell me!

For SPA apps, you could put some state in there, like:

const [isLoading, setIsLoading] = useState(false);

return (
  <Button
  onClick={async () => {
    setIsLoading(true);
    const data = await getSessionUrlFromStripe(id);
    window.location = data.url;
  }
  >{isLoading ? 'Upgrading...' : 'Upgrade to Pro'}</Button>
)

This would provide more immediate feedback. But it also raises a whole set of other questions:

  • Is that actually the interaction you want, where the text changes? That’s probably gonna shift layout. Maybe you want something different, like a spinner in place of the text. How do you handle that?
  • What if you have multiple places to upgrade? Do you have to implement isLoading state in all those places too? What if the trigger in each place is slightly different? A button here, some text there, and icon over yonder? How do you handle all of those different interactions in a standard, immediate way?
  • Errors. What if it fails? Well, we already weren’t handling that in the first code example were we? But maybe we should…

Oh boy, this is getting complicated isn’t it?

This is why, I assume, lots of apps just don’t deal with it.

They accept there will be a slight delay in the responsiveness of the UI (and that it might error, but the user can just click again) and justify that it’s really not that big of a deal if there’s a slight, almost imperceptible delay between clicking a button and seeing the UI respond.

“We’ve got bigger fish to fry.”

And it makes sense. I mean, a slight delay in UI responsiveness, is that why people will or won’t buy your thing? Seems like a small detail. Who’s got the time to spend on details like this?Who cares?

I care. That’s why I’m writing this post.

To my original point, every piece of hardware I currently own is the fastest version of that device I’ve ever had in my life. And yet, everywhere I go I encounter lag. Lag everywhere.

And I’m grumpy about it, hence this post.


Reply via: Email · Mastodon · Bluesky

A Brief History of App Icons From Apple’s Creator Studio

View

I recently updated my collection of macOS icons to include Apple’s new “Creator Studio” family of icons.

Doing this — in tandem with seeing funny things like this post on Mastodon — got me thinking about the history of these icons.

I built a feature on my icon gallery sites that’s useful for comparing icons over time. For example, here’s Keynote:

(Unfortunately, the newest Keynote isn’t part of that collection because I have them linked in my data by their App Store ID and it’s not the same ID anymore for the Creator Studio app — I’m going to have to look at addressing that somehow so they all show up together in my collection.)

That’s one useful way of looking at these icons. But I wanted to see them side-by-side, so I dug them all up.

Now, my collection of macOS icons isn’t complete. It doesn’t show every variant since the beginning of time, but it’s still interesting to see what’s changed within my own collection.

So, without further ado, I present the variants in my collection. The years labeled in the screenshots represent the year in which I added the to my collection (not necessarily the year that Apple changed them).

For convenience, I’ve included a link to the screenshot of icons as they exist in my collection (how I made that page, if you’re interested).

Keynote:

A horizontal row of Apple Keynote app icons from different years—2014, 2015, 2020, 2021, and 2026—showing the evolution of the blue presentation podium icon from a detailed lectern to a simplified, abstract symbol.

Pages:

A horizontal row of Apple Pages app icons labeled 2014, 2015, 2020, 2021, and 2026, showing the evolution from a detailed pen-on-document icon to a simplified, abstract pen symbol on an orange background.

Numbers:

A horizontal row of Apple Numbers app icons labeled 2015, 2020, 2021, and 2026, showing the evolution from a detailed multicolored bar chart on a grid to a simplified, abstract green bar chart symbol.

Final Cut Pro:

A horizontal row of Apple Final Cut app icons labeled 2012, 2015, 2020, 2025, and 2026, showing the evolution from a detailed clapperboard with a colorful light burst to a simplified purple clapperboard symbol.

Compressor:

A horizontal row of Apple Compressor app icons labeled 2011, 2015, 2020, and 2026, showing the evolution from a detailed metallic clamp over film strips to a simplified, abstract golden compression symbol.

Logic Pro:

A horizontal row of Apple Logic Pro app icons labeled 2013, 2015, 2020, and 2026, showing the evolution from a realistic metallic dial on a dark interface to a simplified, abstract blue control knob symbol.

Motion:

A horizontal row of Apple Motion app icons labeled 2013, 2015, 2020, and 2026, showing the evolution from a detailed, metallic orbital graphic around a color wheel to a simplified, abstract magenta motion symbol.

MainStage:

A horizontal row of Apple MainStage app icons labeled 2012, 2015, 2020, and 2026, showing the evolution from a detailed concert pass with a guitarist silhouette to a simplified, abstract teal stage-control symbol.

Pixelmator Pro:

A horizontal row of Pixelmator app icons labeled 2012, 2015, 2018, 2021, and 2026, showing the evolution from a photo-and-brush motif to a simplified, abstract layered-shapes symbol on a red background.

(Granted, Pixelmator wasn’t one of Apple’s own apps until recently but its changes follow the same pattern showing how Apple sets the tone for itself as well as the ecosystem.)

One last non-visual thing I noticed while looking through these icons in my archive. Apple used to call their own apps in the App Store by their name, e.g. “Keynote”. But now Apple seems to have latched on to what the ecosystem does by attaching a description to the name of the app, e.g. “Keynote: Design Presentations”.

  • Keynote -> Keynote: Design Presentations
  • Pages -> Pages: Create Documents
  • Numbers -> Numbers: Make Spreadsheets
  • Final Cut Pro -> Final Cut Pro: Create Video
  • Compressor -> Compressor: Encode Media
  • Logic Pro -> Logic Pro: Make Music
  • MainStage -> MainStage: Perform Live
  • Pixelmator Pro -> Pixelmator Pro: Edit Images

Reply via: Email · Mastodon · Bluesky

Study Finds Obvious Truth Everybody Knows

View

Researchers at Anthropic published their findings around how AI assistance impacts the formation of coding skills:

We found that using AI assistance led to a statistically significant decrease in mastery […] Using AI sped up the task slightly, but this didn’t reach the threshold of statistical significance.

Wait, what? Let me read that again:

using AI assistance led to a statistically significant decrease in mastery

Ouch.

Honestly, the entire articles reads like those pieces you find on the internet with titles such as “Study Finds Exercise Is Good for Your Health” or “Being Kind to Others Makes People Happier”.

Here’s another headline for you: Study Finds Doing Hard Things Leads to Mastery.

Cognitive effort—and even getting painfully stuck—is likely important for fostering mastery.

We already know this. Do we really need a study for this?

So what are their recommendations? Here’s one:

Managers should think intentionally about how to deploy AI tools at scale

Lol, yeah that’s gonna happen. You know what’s gonna happen instead? What always happens when organizational pressures and incentives are aligned to deskill workers.

Oh wait, they already came to that conclusion in the article:

Given time constraints and organizational pressures, junior developers or other professionals may rely on AI to complete tasks as fast as possible at the cost of skill development

AI is like a creditor: they give you a bunch of money and don’t talk about the trade-offs, just the fact that you’ll be more “rich” after they get involved.

Or maybe a better analogy is Rumpelstilskin: the promise is gold, but beware the hidden cost might be your first-born child.


Reply via: Email · Mastodon · Bluesky

Saying “No” In an Age of Abundance

View

You’ve probably heard this famous quote from Steve Jobs about saying ‘no’:

People think focus means saying yes to the thing you’ve got to focus on. But that’s not what it means at all. It means saying no to the hundred other good ideas that there are. You have to pick carefully. I’m actually as proud of the things we haven’t done as the things I have done. Innovation is saying no to 1,000 things.

But wait, we have AI now. We don’t have to say no to 1,000 things. We can say yes to all the things — generate them all, simultaneously!

Do you really have to “pick carefully” when AI can materialize everything you previously would’ve been too constrained to do?

Generative technology paired with being “data-driven” means it’s easy to build every idea, ship it, measure it, and see what sticks.

Humans, money, time — these all used to be constraints which required budgets, trade-offs, and decision making.

Organizations had an incentive to say “no” when development was constrained — “We can only do so much, so let’s make sure we do the most impactful things.”

But maybe the scarcity of organizational resources was the wrong focus all along?

It’s never been a good idea to ship everything you think of. Every addition accretes complexity and comes with a cognitive cost.

Maybe we need to reframe the concept of scarcity from us, the makers of software, to them, the users of software. Their resources are what matter most:

  • Attention (too many features and they can’t all be used, or even tried)
  • Stability (too much frequent change is an impediment to learning a product)
  • Clarity (too many options creates confusion and paralysis)
  • Coherence (too many plots and subplots cannot tell a unified story)

So maybe the way you argue for saying “no” isn’t because it helps you as a business, but because it helps your customers. It helps them make sense of what you’ve made.

And yet: arguing for customer clarity has always been harder than arguing for internal efficiency or some bottom line.

In an age of abundance, restraint becomes the only scarce thing left, which means saying “no” is more valuable than ever.

I’m as proud of the things I haven’t generated as the things I have.


Reply via: Email · Mastodon · Bluesky

The Browser’s Little White Lies

View

So I’m making a thing and I want it to be styled different if the link’s been visited.

Rather than build something myself in JavaScript, I figure I’ll just hook into the browser’s mechanism for tracking if a link’s been visited (a sensible approach, if I do say so myself).

Why write JavaScript when a little CSS will do? So I craft this:

.entry:has(a:visited) {
  opacity: .5;
  filter: grayscale(1);
}

But it doesn’t work.

:has() is relatively new, and I’ve been known to muff it, so it’s probably just a syntax issue.

I start researching.

Wouldn’t you know it? We can’t have nice things. :visited doesn’t always work like you’d expect because we (not me, mind you) exploited it.

Here’s MDN:

You can style visited links, but there are limits to which styles you can use.

While :has() is not mentioned specifically, other tricks like sibling selectors are:

When using a sibling selector, such as :visited + span, the adjacent element (span in this example) is styled as though the link were unvisited.

Why? You guessed it. Security and privacy reasons.

If it were not so, somebody could come along with a little JavaScript and uncover a user’s browsing history (imagine, for example, setting styles for visited and unvisited links, then using window.getComputedStyle and checking style computations).

MDN says browsers tell little white lies:

To preserve users' privacy, browsers lie to web applications under certain circumstances

So, from what I can tell, when I write .entry:has(a:visited) the browser is telling the engine that handles styling that all .entry items have never been :visited (even if they have been).

So where does that leave me?

Now I will abandon CSS and go use JavaScript for something only JavaScript can do.

That’s a good reason for JS.


Reply via: Email · Mastodon · Bluesky

The Don’t “Contact Us” Page

View

Nic Chan comes out as the whistleblower on how many “Contact Us” pages are made (spoiler: they’re designed to keep us from contacting anyone).

A “fuck off contact page” is what a company throws together when they actually don’t want anyone to contact them at all. They […] are trying to reduce the amount of money they spend on support by carefully hiding the real support channels […] If you solve your own problem by reading the knowledge base, then this is a win for the company. They don’t want to hear from you, they want you to fuck off.

It’s true. This is how the proverbial sausage is made. I’ve been there. I’ve seen these decisions handed down. Which means, like Chan, I know how to read between the lines of most “Contact Us” pages on the internet.

I’m not sure about you, but as a user, when I see [these kinds of pages], knowing that whatever my original query was, [I know] I’m going to have to solve it unassisted.

My process follows this arc:

  • I have a question.
  • Go to the company’s “Contact Us” page.
  • Immediately intuit from the design of the page whether I’m actually going to be able to contact someone and get help, or if I’m on my own.

A direct line to a human is the ultimate luxury in today’s world.

The project finished on time, everyone got paid, and the client was happy with the end result, but I still felt very disappointed in the whole thing.

So it goes.


There’s a scene from The Matrix that kept echoing in my head while reading Chan’s post.

There are contact pages, my friends. Endless “Contact Us” pages.

Where human beings no longer exist.

For a long time I probably wouldn’t have believed it, and then I saw the pages made with my own eyes. Watched them remove the ability for human beings to contact one another.

And standing there, facing the pure, automated precision of it all, I came to realize the obviousness of the truth.

What is the “Contact Us” page?

Cost savings.

The “Contact Us” page is a computer-generated dream world, built to keep us from contacting another human in order to save cost and turn a human being into this: a source of revenue.


Reply via: Email · Mastodon · Bluesky

You Can Just Say No to the Data

View

“The data doesn’t lie.”

I imagine that’s what the cigarette companies said.

“The data doesn’t lie. People want this stuff. They’re buying it in droves. We’re merely giving them what they want.”

Which sounds more like an attempt at exoneration than a reason to exist.

Demand can be engineered. “We’re giving them what they want” ignores how desire is shaped, even engineered (algorithms, dark patterns, growth hacking, etc.).

Appealing to data as the ultimate authority — especially when fueled by engineered desire — isn’t neutrality, it’s an abdication of responsibility.

Satiating human desire is not the highest aspiration.

We can do so much more than merely supply what the data says is in demand.

Stated as a principle:

Values over data.

Data tells you what people consume, not what you should make. Values, ethics, vision, those can help you with the “should”.

“What is happening?” and “What should happen?” are two completely different questions and should be dealt with as such.

The more powerful our ability to understand demand, the more important our responsibility to decide whether to respond to it. We can choose not to build something, even though the data suggests we should. We can say no to the data.

Data can tell you what people clicked on, even help you predict what people will click on, but you get to decide what you will profit from.


Reply via: Email · Mastodon · Bluesky