More from Jim Nielsen’s Blog
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
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
(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
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
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
More in design
My feed is mostly designers talking about AI, and the temperature of all of it is the same: Something is coming. You need to get out in front of it. You need to be the one doing the changing rather than the one being changed. I don’t disagree with most of the individual advice. But read enough of it in one sitting and what surfaces underneath is competition — ordinary competition, of the kind that has always been there. The worry takes two shapes. Either the client no longer needs a designer, because the tool supplies the design. Or another designer, holding the tool, takes the job you would have had. Both are worth taking seriously. Neither is new. The Permanent Condition There has always been someone faster than me, someone cheaper than me, and someone better than me. This was true the day I started my career and it will be true on the day it ends. It describes a market, not an era. Designers — actually, all in the professional services — have a folk theorem for living with this reality: Fast, cheap, good — pick two. I’ve heard it said and read it written countless times. I’ve said it and written it, too, to win arguments and contracts. Today I got curious about where the saying comes from, and went looking. Nobody knows. The phrase itself gets attributed to something called the Common Law of Business Balance, and from there to John Ruskin — an attribution with no evidence behind it whatsoever. The idea underneath has a slightly better paper trail: an engineer named Martin Barnes drew it as a triangle, with cost, time and quality at the corners, for a project management course in 1969. Even that isn’t an origin. Versions of it were in use by the 1950s, and no one can say who put it that way first. This beaten-to-cliché piece of professional wisdom is a cipher. Which is useful to remember because it’s also, often, wrong. There are as many designers who are fast and cheap and good as there are those who are slow, expensive, and bad. I have worked with some of each. Thinking back on this now, I am surprised that no one ever responded to the “pick two” challenge by saying, “I want all three, I just won’t be getting them from you.” Faster and Cheaper My entire career has been punctuated by tools that sped up production. Desktop publishing collapsed typesetting and layout into something one person could do in an afternoon. Then Photoshop. Then the rest of the Adobe suite, Illustrator and InDesign. Then Sketch. Then Figma. Each one arrived, each one made producing the work materially faster, and each one set off a race to master it before everybody else did. Each of them may have changed what a day at work felt like, but none ended the profession. What makes faster feel dangerous is that we convert it immediately into cheaper, and we do that because most designers price their time. If the work takes four hours instead of twelve, charging the twelve-hour price feels indefensible, and somebody else will happily charge for four. That’s a pricing problem rather than a technology problem, and it’s been the same pricing problem through every acceleration I just listed. A practice that charges for outcomes absorbs a speed increase as margin. A practice that charges for hours absorbs it as a pay cut. As for cheaper, it arrived long before any of this, and it arrived by geography. I work closely with a designer who lives in the Balkans. They are, by some distance, among the most talented people I have ever worked with. They didn’t go to design school. I went to the Rhode Island School of Design, which bought me a credential the marketplace still reads as shine, and I’m clear-eyed about the fact that on the measure that actually matters they aren’t behind me. They can also charge less than I can, because rent where they live isn’t rent where I live. None of that has anything to do with AI. It has been available to every client I have ever had as long as we all had an internet connection. And just in case anyone thinks that professional design competition from overseas is a more recent phenomenon, the first designer I met when I visited Malaysia in 2004 was born and raised in Penang, had no design degree, and had dozens of clients most of whom were headquartered in the United States. He was fast, cheap, and good. That’s why they paid him, rather than someone else. What Better Means There is an objection to what I’ve said so far. I’ll try to put it in its best form rather than a convenient one. Every tool I listed made production faster without making the thing. Desktop publishing didn’t design the page. Photoshop didn’t choose the image. Figma doesn’t know what the product should be. AI, as a design tool, is different in kind, because it produces the artifact. So the comparison to Sketch and Illustrator is a weaker argument than it sounds. I think that objection is right about the tool and wrong about the job. It only holds if the artifact itself is better. Imagine two designers given the same brief — an identity system for the same client. Put the results in front of a panel, blind, with no knowledge of how either came to be. The panel picks a winner. They’re qualified to do it and their reasoning will be sound. Now ask the client which designer they’d hire again. It is by no means a given that the panel and the client will agree. The two answers can fork, and when they do it isn’t because the client has poor taste. The panel is assessing an artifact. The client went through something. They were asked questions nobody had asked them before. They had a position drawn out of them that they couldn’t have articulated on their own. At some point they were told that the thing they wanted was the wrong thing, by somebody who had earned enough credit to say so. None of that is necessarily visible in the file — nor should it be. I wrote recently that expertise is becoming subjective — that where knowledge is cheap, experience is what differentiates. If that’s true, it cuts in both directions. Experience doesn’t only distinguish you; it aligns you with some people and not others. A prospect’s fit with how you’ve worked, what you’ve lived through, and how you talk about it is doing as much work as the expertise itself. Which means there’s no better in the abstract. There’s better, for them. There’s also a posture question here, and it matters more than it looks. To believe that your prospect couldn’t do better than you is a level of audacity that runs counter to how every designer I know actually thinks — and yet it’s close to what we’re advised to project. In a world this populous it’s absurd to imagine you’re the best at anything, and more absurd still to build a public persona on the premise. That advice persists because it works on a seductive technicality: whether or not you believe you’re the best, behaving as though you are turns out to be convincing to other people. Notice who that advice is sold to. We’re the market for it; our clients have never heard of it. Rejecting that is accuracy rather than modesty. The world is too crowded with designers to spend a second ranking yourself among them. Beside the posture, the same feed sells a tempo, and that carries its own irony. All that commentary argues that you should move faster in order to stay ahead. But if experience is the differentiator, speed is the thing eroding it. Going slowly through a problem — getting it wrong, trying it on a real client, finding out — is where the differentiation was built all along. The race is being run on the wrong road, and running it costs you the one you needed. Nobody Asks About Tools When I moved from Sketch to Figma I thought it mattered. I had reasons. I argued about it. Designers I respected stayed on Sketch for years afterward and we all thought the other was wrong. In the end, it didn’t really matter. I know designers who still work primarily in Photoshop. In the last twelve months, I saw a website design produced in Illustrator. Of course I think these are mistakes, bordering on the absurd. But it’s also none of my business, and as far as I can tell it hasn’t cost them a job. Isn’t that interesting? But it makes some sense. After all, nobody has ever asked me what tools I use. Except another designer. I don’t know what this technology does to the profession over twenty years, and I notice that the people who claim to know are usually the ones with something to sell. What I can see from here is smaller and steadier than that. Somebody out there is faster than me. Somebody is cheaper. Somebody is better — that isn’t modesty, it’s the realism that comes from decades in the work, and I have met them. All three have been a phone call away from every client I have ever worked with. Any prospect can have them, faster, and at a cheaper price. But they don’t have me. And they don’t have you.
If you work in software (as a designer, developer, PM, or any adjacent role), you’re probably aware of just how much AI has changed the way code is created. And if you’re an engineer, you’re definitely feeling the downstream impacts of AI’s speed. Way, way more code is being written. But the way we review code hasn’t changed that much. But if you’re not familiar, code review is a step in the software development process where developers look at code before it gets shipped. It’s been a cornerstone of engineering teams’ workflows for almost as long as there have been engineering teams. AI coding has turned code review into a bottleneck; a human can only look at so much code, so reviews have started taking longer, and each engineer’s review backlog is growing exponentially.1 AI code review tools have stepped into the breach, eliciting an earnest grumble from developers: AI reviewing AI-written code is like a student grading their own test. Execs don’t have much sympathy, though. We can move faster, so we should. So teams are stuck in a traffic jam, trying to figure out how to review code faster, without sacrificing quality. I’d like to argue that the source of this problem is more fundamental than speed vs. quality. We’re in this situation because somewhere along the way we swapped one question for another. Code review was always meant as a verification step: does the code produce the outcome we want? But it’s a hard question to answer, so we started asking an easier one instead: is this good code? Let’s go back to the original question. Code review should get better at verifying outcomes — faster, yes, but more accurately, and for the system as a whole, not just single lines of code. What is code worth? The premise of code review is that code can be good or bad, and that bad code can be turned into good code, and that good code is important. This is a sound premise. Bugs are obviously bad for business. But we’ve made an attribute substitution: we decide whether code looks good as a stand-in for whether the software works. Engineers use mental shortcuts to determine the goodness or badness of code. They range from very well-documented patterns (use guard clauses to return early, use pure functions where possible) to vibe-based ‘code smells’ (magic numbers, deep nesting). But these aren’t written in stone. They are debated and re-evaluated constantly. Even if there are inviolable definitions of good code, writing good code isn’t guaranteed to result in valuable software products. Sometimes, it works out that way; before Facebook bought it, WhatsApp kept its codebase lean enough that 35 engineers could serve 450 million users.2 But there are far, far more examples of software succeeding despite being written poorly: Undertale would never have passed a single code review (almost all of its dialog was one giant switch statement), but became one of the most beloved games of the past decade. Good code does not strictly equal good software. Code itself has no intrinsic value. So what are we actually reviewing when we review code? Verifying There’s just no substitute for actually running the code and verifying that it does its job well. I don’t mean unit tests, or even integration tests, but running the code in total, a full end-to-end test. Doing this at scale for large applications is difficult. At an org with hundreds of AI-enabled engineers, you likely have thousands of pull requests a day. Spinning up and breaking down complete copies of your entire application (likely, application cluster) is time- and resource-intensive. Even if you do manage it, verifying under realistic conditions may be completely infeasible (imagine simulating the activity that Stripe handles — 1.75% of global GDP). Which is to say that you won’t know if your code works or not until it’s in users’ hands. Traditional code review has two outcomes: You are confident enough that the code works well enough to ship it You are not confident enough that the code works well enough to ship it Verifying your code under the most realistic scenario possible (i.e., yolo, straight to production) has three: You are sure the code works, and it’s already shipped. Good job! You are sure the code doesn’t work, so you improve the code until it works You don’t know because the system failed in an interesting way. Instead of confidence, you have information, and can use it to improve the system. Verifying a major change by shipping it is risky, but the closer you can get to it, the more your review is actually worth. Feature flags, canary releases, observability and CI/CD — the stuff of modern devops — have all been commoditized to the point where it’s cheap to build in the safeguards that make the yolo approach tractable (even at Stripe scale). Stop reviewing code3 You’re probably doing code review for the wrong reasons. Let AI do the work of making code better: tracing code paths, finding, and fixing obvious issues. And if it’s a student grading their own test, so what? AI can make the code better. People can check if it works. Invest in the tools to verify your code in realistic conditions, deploying it directly to users if possible. Use your own eyes and brain to see it working, or see it not working. Learn from the difference, share what you learn with your team, and build feedback loops that improve the system the more it fails. And eventually the failures will become rare and incredibly interesting. Footnotes & References GitHub saw 1 billion commits in all of 2025; by April 2026 it was seeing 275 million a week. ↩︎ Cade Metz, “Why WhatsApp Only Needs 50 Engineers for Its 900M Users,” Wired, September 15, 2015. ↩︎ Of course, there are situations where safety means protecting human lives or massive economic outcomes, and reviewing code is the only safe option. The IRS shouldn’t ship updates to the code it uses to disburse tax refunds without reviewing the code. Waymo shouldn’t ship updates to its self-driving cars’ algorithms without reviewing the code. But you probably aren’t working for the IRS or Waymo. And if you are, get in touch ;) ↩︎
If you think about it, there is *a lot* of software that hasn’t been built across hundreds of industries because our best paid designers and engineers were occupied with building social networks and ad networks. "The best minds of my generation are thinking about how to make
Weekly curated resources for designers — thinkers and makers.