Posts in Frontend (20 found)
Unsung 5 days ago

When commit means cancel

A strange thing happens when you press Enter on an empty item in a list in most text editors – the entire list item disappears: This feels counterintuitive. Isn’t Enter for committing and adding more things? Wouldn’t Backspace be the right key to press to break a list? Yes, and no. I am not sure who invented this pattern (I spotted it first in Word 95), but that someone understood a strange interaction contract existing in text editing – Enter is actually an escape hatch. In text editing, no matter where you are, you can always press Enter multiple times to just create more room for writing. In an app that doesn’t cancel a list on Enter, you can face a terrifying moment where you get stuck in a list, and getting stuck is never fun. This principle feels so useful that I see more and more apps apply a version of it for other things. For example, in many modern text editors pressing Enter after a headline returns you to regular text, just so it’s not as easy to get stuck in a headline style: #errors #flow #keyboard #text editing

0 views

New Declarative Website Menu with Invoker Commands and noscript Hacks!

Read on the website: I updated my website menu to be prettier on mobile, and I did not sacrifice accessibility and noJS folks! Go check it out and adopt it!

0 views
Den Odell 1 weeks ago

Your SPA Is Leaking Memory. Soak Test It

Memory leaks are a constant battle for backend teams. A server stays up for weeks, responding to requests the whole time, and if any part of the code running on it has a memory leak, even a small one, that server will eventually run out of memory and crash or restart. So how do these teams know their services won’t end up like this? They soak test them. In a soak test, a team points a script at their server and has it send fake traffic for hours at a time, sometimes thousands of requests a minute. These tests are often automated to run overnight, while the developers are away, and they compare the service’s memory at the end against the baseline from the start. If the memory climbed while the test ran, there’s a leak somewhere in the code, so the test fails and the team has to find it and fix it before the service goes live. Frontend code never used to have this problem, because clicking a link to a new page destroys the memory used by the old page. On a page that only lasted minutes, any potential memory leak was gone before it could grow into a problem. But the web has changed a lot in the last decade. Single-page web apps (SPAs) give you an experience that feels more like a native app than a website. It’s smoother to use, but it means the page is never reloaded and nothing forces a full reset of its memory any more, so if any part of the frontend code running on it has a memory leak, even a small one, that browser tab will eventually run out of memory and crash or reload. I know of teams who force a hard reload of their SPAs every few hours just to avoid this. Electron apps work the same way, along with anything else built around a long-lived web view, since the page underneath is never reloaded either. A static analysis of 500 popular React, Vue and Angular repositories , published in early 2026, found that 86% of them set up a listener, timer or subscription somewhere and never remove it. So how do you know your SPA won’t end up like this? You soak test the frontend too. Gmail was doing this over a decade ago , running memory checks in its pre-release tests for hours at a time, after leaks left some users reporting processes over 10GB. Unless you’re deliberately running something like Meta’s MemLab , your existing tests probably aren’t set up to do this for you. Your Playwright end-to-end suite is the closest thing you have, since it actually clicks around the app, but it still starts each of its tests with a new browser context. It starts from the same place every time and finishes quickly. That’s what you want from a test suite the rest of the time, but a leak needs longer than one test to get big enough to measure. Someone with the app open all day works through the same screens over and over. Detached nodes stay in memory, kept alive by listeners still attached to them, while timers keep firing and the cache keeps growing. To make a frontend soak test, you construct a user flow yourself and run it on a loop, all inside a single browser context. We’ll use Playwright for this, since it’s probably already running your end-to-end tests. The flow starts and finishes on the same screen, so each pass leaves the app where it began. Your simulated clicks act like fake traffic, and since Playwright clicks as fast as the app can keep up, a few hundred loops take minutes rather than hours. If the app is back on the screen it started on, its memory should be close to where it started too. You’re watching for memory that keeps climbing loop after loop. The memory only comes back to where it started if the flow is a round trip, like opening a drawer and closing it, or filtering a table and clearing the filter. Some apps are meant to use more memory as they go, of course, so not every flow can be a soak test. Scrolling a feed that loads more as you go is supposed to end heavier than it started. A chat interface might be too, if you don’t delete the messages after they arrive. Chrome will tell you how much memory the page is using over the Chrome DevTools Protocol (CDP), which is what DevTools itself uses. That limits this to Chromium browsers, since Playwright can only open a CDP session there. We collect garbage twice, for reasons I’ll come back to, then ask for the page’s metrics, which come back as a long list including the heap size, the DOM node count and the listener count: The heap size moves around between runs whatever your app does, so it’s the node and listener counts we’ll assert on. Now we’ll add a function around it. It takes the flow you want repeated and runs it 200 times against a single browser context, taking a reading after a short warmup and another at the end: The first time the drawer opens, the browser has to fetch its JavaScript and the app has to fetch its data, and both stay in memory afterwards. That only happens once, on the first loop. If you take the baseline before it, the heap jumps between your two readings and the test fails, when all that grew was the code and data the drawer needed. So runs five loops before it takes the baseline. The node and listener counts don’t need the warmup. They climb by the same amount from the first loop even on an app with lazy routes and a query cache, because lazy-loaded code and cached data live in the JavaScript heap, while the counts go up when the page gains a DOM node or a listener. Five loops take a couple of seconds, so I leave them in anyway. The second call is there to make the node count reliable. On a React app I tried this on, one pass left a detached drawer in the count on about one reading in six, so a healthy test run would fail. I found a plain page with pure JavaScript and no framework always came back clean after one pass of garbage collection. That leaves us with a test that’s just the flow and the assertion: Leaks normally get found by someone taking heap snapshots and reading through them, which is painful and slow. A node count is just a number, so a test can compare the two readings for you. Readings vary between runs, so this belongs in a nightly job rather than on every pull request. Where the leak involves a listener, that count is the one to assert on, since it only goes up when your code adds a listener, and down when it removes one. The node count catches leaks that hold on to DOM with no listener attached, and a fixed allowance works better there than a percentage, because a drawer sitting at 33 nodes between passes makes one stray node look enormous, while the same node against a 2,000-node baseline is nothing. I used in my code because it’s a nice, round number, above the jitter I saw in initial results and far below what even a small leak would add across 200 loops. That soak test still misses the biggest category in that 500 repository scan, though. Timers left without being cleaned up made up nearly 44% of everything it found, most of that . They sit on the browser’s clock, so a polling check set to run every 30 seconds fires twice a minute, and after 200 loops in two minutes it has fired four times, where an hour of real use would have fired it 120 times. You can work around this mismatch by faking the browser clock. Playwright can replace everything the page uses to tell the time, from and to timers and animation frame callbacks. Installing it before the SPA loads lets the page start up normally. on its own leaves time flowing, though, so you pause the clock once the app is up, and from there it only moves when says so. Advancing 18 seconds on each of the 200 passes adds up to an hour of timers across the run: The clock only fakes timers, so a still takes as long as it takes, and whatever comes back is stored by your app and stays in memory. A poller often calls again once each response arrives, so requests never overlap when the server is slow: This next bit is fiddly and it took me a few goes to get straight. There are two clocks running, a fake one that only moves when tells it to, and the real one, which keeps going the whole time. fires the pending timeout, runs until it hits the , and the call returns while the request is still out. The response lands during the next pass, in real time, and then schedules its next timeout from wherever the fake clock stopped. Your API speed sets the polling rate now, not the 30 seconds you asked for, and across the run that works out at roughly 100 requests where an hour of real use would make 120. The fix is to mock the network as well. We answer each request ourselves, then advance 30 seconds at a time, waiting for the response before the next tick: That covers 200 rounds of clicks and 100 minutes of polling, with every response landing before the clock moves again. What you send back wants to be close to what your API actually returns, ideally identical. If you return 200 bytes where the real endpoint returns 50 kB, the leak in your test is hundreds of times smaller than the one in production, and the test passes. For web sockets, does the same job, letting you send messages to the app as fast or as slow as you want, set up before you navigate just like the clock. Streamed responses, like those used in AI interfaces, are the one awkward case, since only takes a string or a buffer, so you can’t send the response in pieces at a speed you choose. Those counts tell you there’s a leak, but not where it comes from. For that you take a heap snapshot in the Memory panel of Chrome DevTools, then type into the class filter, which leaves you with the DOM your app removed from the page and still has a reference to. Clicking one shows its retainers underneath, so you can see which listener or variable still references it. A soak test is one flow, repeated a few hundred times, with the DOM node and listener counts read before and after. Faking the clock and the network lets it run in compressed time, so a short test run covers hours of real use. You get an answer straight away, and you can leave it running overnight with the rest of your automated tests. This is what I mean when I talk about Fast by Default . You write the test before anything goes wrong, so the problem shows up, and gets fixed, before it reaches production. Most teams find out about a memory leak the day a customer says the app goes slow after leaving it open a few hours. Now you have to find it across the whole app and its Git history, when a soak test would have failed the night someone committed the leak. Going by that repository scan we saw earlier, most codebases leave a listener or timer registered somewhere and their development teams have no idea. That pattern, where performance problems only show up once users hit them and someone drops everything to patch it, is what my book Fast by Default: Practical Performance Engineering is all about fixing. This soak test is one small example of it. The book applies the same approach to loading, rendering and everything else users wait on, and argues for performance being something the whole team owns rather than one person’s job. It’s in early access now, so the chapters are going up as I write them.

0 views

The Difference Between a Button and a Link

Of the three proposals in the Triptych Project , my multi-year odyssey to add a few small-but-powerful features to HTML, the one that generates the most questions is Button Actions . The proposal itself is very straightforward: we want to add the and attributes to the button. Button Actions are such a simple primitive that people often ask why they’re needed. The answer rests on a distinction that web users intuitively understand but rarely have to think about directly: the difference between a button and a link. I added a detailed “Buttons vs Links” section to the proposal, but I think it deserves a blog-style explanation as well, because most of the existing ones miss the mark. Links represent a destination while buttons represent an action . Functionally, this means that links let users control what context they open in, while buttons don’t. Web browsers offer countless affordances for re-contextualizing a link. Clicking or tapping the link will navigate the current page to that destination. Mouse users can middle-click the link to open it in a new tab or hover over the link to see where it goes. Context menus (right-click on desktop, long tap on mobile) have lots of link-specific options. Web users are very familiar with the features that come with links. They know how to open them, copy them, bookmark them, share them with friends, and maintain them in an inadvisable number of browser tabs. The semantics of a link—the notion that they represent an independently-navigable destination—make it possible for browsers to build all these features. The hyperlink predates the invention of the browser tab, but when browsers added tabs, websites didn’t have to do anything to support them; links represented destinations that could be re-contextualized, so browsers could simply invent a new context for them to open in. Every website instantly got upgraded with a huge new feature. Buttons have none of these features. By default, they cannot be middle-clicked, control-clicked, or hovered over for more information. Buttons don’t allow you to copy their the way you can copy the of a link. Their context menus contain no affordances for saving the action or doing it somewhere else. These are not omissions, but deliberate choices based on the button’s semantics: buttons trigger actions inside a specific browsing context ( almost always the current one ). Copying, sharing, bookmarking—these are all features for re-contextualizing the action of a link. Buttons serve a complimentary purpose because they don’t allow for any of that. A common misconception is that links are for navigating the page, while buttons are for everything else. This is incorrect on both counts. Buttons regularly perform navigations. Clicking a logout button navigates the current page to a logged-out one; clicking a “search” button navigates the current page to the query results. Both of these are navigations in the HTML standard . They change the URL, they get logged in the session history, and they load a new page. And links are often used in situations where they don’t trigger navigations. Relative links can jump around the current page; mailto links can open email clients; download links can save a file to your computer. None of these are navigations, but they are all “destinations” that can be opened, saved, and shared in customizable ways. Navigations should be represented as buttons when their action happens in a fixed context that is not available to be re-contextualized (e.g. bookmarked, shared, middle-clicked, etc.). A frequent place this comes up is with forms that let you edit something you’ve already saved, like a comment on a website. When you click “Edit”, the website shows you an editable text area with options like this: Users will easily intuit what each button does: Should “Cancel” be a link? No! Its job is to close the edit view. Not only does making this a link incorrectly communicate its purpose—visually and otherwise—but it saddles the form “control” with lots of features, like bookmarking and middle-clicking, that have incorrect behavior. There are many plausible ways these buttons could be implemented, but none of those implementations should present themselves to the user as a link. With Button Actions, this entire UX could be implemented with just HTML. The first two buttons use existing HTML features, the second two buttons are made possible by Button Actions. (I’m also taking advantage of Triptych’s DELETE support , but you could do the URL method hack without it.) Philosophically, Button Actions create a generic control that can redraw the current context with a network request. Buttons already have the ability to do this with certain limitations; this proposal removes those limitations. Practically, this allows web authors to implement state transitions by navigating to views. Those views might even already exist as standalone destinations, in which case authors can trivially re-use existing routes while representing the action correctly in the UI. This is a great pattern that HTML should encourage! Unfortunately, without Button Actions, erroneously making this button a link is the only way that we have to implement this interface without scripting. This is obviously an anti-pattern, but it’s an anti-pattern supported by major design systems, because buttons lack the ability to do basic navigation without forms. When building a website that works without JavaScript ( a requirement for UK government sites ), links are the only choice. The US Web Design System (USWDS) even contains an official affordance for it: Add to a link and it will look like a button. Making a link look like a button, however, does not make the link behave like a button. USWDS uses JavaScript to implement spacebar activation , but JavaScript can’t do anything about the litany of other behaviors that differentiate buttons from links, like context menus. Links (even those with ) will still look like links in reader mode or other custom views. That’s the fundamental consequence of violating HTML semantics—the page will be broken for some users because authors cannot possibly account for all the different ways that people interact with a web page. The web simply wouldn’t work if they had to. Navigations are the broadest tool that web authors have to control the user experience—HTML just needs to complete the ’s ability to trigger them. Doing so makes the web simpler, safer, and more accessible for all. If you’d like to support the effort, the best way is to like the Button Actions issue on GitHub and share examples of why the proposal would be valuable to you. “Save” updates the comment with whatever is in the “Save Draft” saves the content of the without publishing it “Cancel” closes the editable form “Delete” removes the comment entirely Big shoutout to The Django Software Foundation for their support of this proposal ! I am currently working on an analysis to demonstrate that Button Actions do not introduce any new XSS vulnerabilities to existing web sites. Supporting this proposal doesn’t resolve that issue, but it does demonstrate to WHATWG that web authors have this need and that it’s worth studying. This blog focuses on buttons that trigger GET requests without forms, because that’s where the overlap with links is, but buttons that trigger unsafe requests without forms are also very useful. requests are probably the most common use-case, because they usually don’t require any additional data. One interesting case for buttons that trigger or requests without a form is “likes” on social sites . HackerNews , for instance, uses links for upvotes, which is in wild violation of HTTP semantics. I understand why they do it though: it’s simpler and works without JavaScript. That’s why it’s necessary to make Button Actions not just possible, but convenient. The proposal addresses all the existing workarounds for the lack of this functionality and explains why they’re not sufficient. The big picture goal with Triptych to is to give web authors a simple and semantic way to model a full CRUD lifecycle in HTML, because that’s all the vast majority of web services need to do. All the Triptych Proposals complement each other—Button Actions are even more useful with additional methods and partial page replacement —but I try to make the case for each one in isolation, both as an anti-logrolling mechanism and because they are genuinely useful on their own.

0 views
Unsung 2 weeks ago

Five moments in snapping history

Bear (a notetaking tool) has simple image resizing, with one extra nicety – if your images are near each other, resizing one will snap to the width of the other: In the Finder, columns snap to the width necessary to keep all the names untruncated – and not one pixel more: (Sidebar: This is also the only place I’m mentioning today that nicely uses the trackpad’s haptic feedback at the snap moment. I tried to indicate it in the video; this is not the final visual treatment I’m thinking of, but let me know if this kind of visualization of haptics feels useful to you!) macOS does something really interesting when you get its windows close to each other. Instead of typical snapping – pulling the thing you hold toward the other item like a magnet – it instead prevents you from going further for a while, in either direction. Perhaps the right analog here would be glue : If initially feels a bit funny, but I think I like it. It’s less aggressive and avoids needing some sort of cancellation (an option or a modifier key) if you don’t want it, because it never feels in a way. It also works for matching heights, like in Bear: In Figma, building atop regular snapping, we introduced something I awkwardly called “self-snapping”: if your objects are inside a container, the container will snap its padding to whatever it sees on the other side, without any explicit auto layout/​flexbox: I am sharing these five examples (and one from before ) because I think they exemplify a nice thing: precision without bureaucracy. In each case you could imagine an explicit heavy option somewhere in the menu… …that would feel slow and cumbersome. Instead, these take the freedom of direct manipulation and sprinkle just enough almost-invisible structure in a moment where that structure is undeniably useful. (Of course, you still might want explicit options somewhere in the menu or your command palette, if only for accessibility reasons.) I like that these quiet features have your back and make you look good, and that their creators understand that something almost aligned can feel worse than something completely misaligned. #bear #complexity #direct manipulation #interface design #mouse Bear: Set Image Width… macOS: Match Window Heights Finder: Restore Column Width Figma: Unify Padding

0 views
Unsung 3 weeks ago

“No such thing as too fast”

On Mastodon , Alex Russell, a product architect who’s worked on Chrome and Edge, and has focused on tech standards for a while: Once Upon A Time At Google, a team presented results that had confounded them: making the system load several times faster increased engagement somewhat, but in line with Tammy’s findings, engagement went way up for every 100ms improvement below the 1s threshold. Going fast enough to become “dial tone” changed user behaviour and expectations in a hugely positive way for the product. This sort of “no such thing as too fast until you prove it” lesson is everywhere . Phrasing it as “no such thing as too fast” is really interesting, and not something I encountered before. (The way I understand the “dial tone” remark is commenting on reliability of landline phones in the second half of last century. The landlines were extremely reliable and even came with their own power source; you could pick up the handset and the dial tone – the system’s confirmation it’s ready for you to dial – was inevitably and immediately always there, already waiting for you. There was never any delay when the phone had to get ready for you to call.) Russell links to a report by Tammy Everts : If you make websites for a living, stop what you’re doing and read this research by Tammy Everts; it shows what many of us have been saying for a long time: even if there is such a thing as “fast enough” (there isn’t), it’s generally much faster than you are targeting. The report itself is perhaps too deep and jargony for this blog, but the TL; DR seems to be: Google suggests the time for the site to finish loading its largest piece is 2.5 seconds, and Everts argues and shows evidence that it’s a lot less. I have before focused on “finger speed” – making sure the interactions operate at the “speed of flow,” which requires sweating speeds counted in milliseconds. Everts’s and Russell’s comments confirm that millisecond-speeds matter for other reasons, too. #performance #web

0 views
Unsung 3 weeks ago

“If HEIC has no haters I’m dead.”

Over on Bluesky, Melanie Walsh asks : Favorite and least favorite file formats? I’ll start. Favorite: TXT Least favorite: HEIC The answers – both replies and quote posts – are really interesting because most of the time they’re not about inherent capabilities of each format, but: Of course, Walsh put a finger on the scale with her initial example, but HEIC stands out as a favorite least favorite. I understand this is mostly out of its limited support, raising a question whether Apple spent the right amount of time socializing and incentivizing its adoption – even on a Mac, you can’t escape blank stares the moment you drag it into many websites/web apps: HEIC on the other hand, Apple’s way of making photos smaller and everything else more complicated than it needs to be. By the way HEIC is when you drag a picture from your Notes app into your email, and then it laughs in your face and is like sorry, girl, I’m HEIC!! I don’t do things like that!! I didn’t know I had a least favorite file format but yeah HEIC can fuck right off Sweet fucking hell fuck heic into the sun Reading the replies here makes me feel like I live in an oddly privileged bubble in an inverse of the usual meaning of privilege for being a poor Android-using mfer who has never seen a HEIC in their life and had to actually look that sh*t up. Least favorite is a toss up between HEIC (WHICH NOBODY ASKED FOR, APPLE) and WEBP Controversial but I hope everyone involved with HEIC only tastes soap instead of cilantro forever I agree with this person that WebP is much better supported than it used to, but it sometimes takes one link in the chain – cough Google Docs cough – for you to avoid a format forever. And, those are always lagging indicators. If a format didn’t work once in an important flow, it might take many years before you come back: all the people saying “webp” in the quotes might as well be fighting WW2 still. look for another grievance. please Some other fun answers: IF IT’S CALLED [C]OMMA [S]EPARATED [V]ALUES WHY DO I HAVE TO OPEN A WINDOW AND CHANGE THE DEFAULT DELIMITER OPTION FROM TAB TO COMMA ??!?!?! Favorite: MP3 (invented piracy, patents all expired, doesn’t need an FPU) Least favorite: DICOM (nightmarish metadata, too many possible image encodings, when it wants a 3D volume the solution is just “a bunch of files in a folder”, also IT IS A NETWORK PROTOCOL >:( ) Least fave: .R01, .R02, etc... – nothing needs to be split into multiple rar files! Please stop! The world has moved beyond this. Least favorite: can I count those awful pointer doc types Google uses, like .gdoc and .gsheet favorite: transparent PNG least favorite: transparent PNG that is not really transparent but just a fuckin checkered background I forgot about this meme: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-heic-has-no-haters-im-dead/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-heic-has-no-haters-im-dead/1.1600w.avif" type="image/avif"> For least fav I voted for GIF, having not only spent countless hours trying to make good-looking animated gifs that do not weigh tens of megabytes, look horrible, and cause performance issues… but also having worked on two different products (Medium and Figma) that had to swallow gifs made by others, and seeing engineers lose their minds peeking into their insides and how messy they were . To be fair, GIF comes from the late 1980s, and simply outlived its purpose. It’s a fascinating format that literally deserves a book written about it: the messy patent wars, the pronunciation, the technical format and many surprises hiding inside , even the word “gifs” transcending the format itself to mean “short animated memes.” To go back to the thread, a small pattern that I also encountered from time to time: Least favorite: .md, specifically when it’s used for Sega Genesis game roms. There’s already a type of text file type called .md, so Windows tries to open them in notepad. Just call it .gen instead, nerd. Favorite: TS, the one that opens in my IDE Least Favorite: TS, the one that opens in Quicktime Lastly, because of course someone had to do it: Favorite: Gaylord Archival® Reinforced Acid Free Manilla Least favorite: Office Depot Vertical Hanging Folders #encoding #graphics #software evolution how well supported it is in the general ecosystem? how painful it was last time I used it? who’s using it and for what? if there is one app I use it with, do I like this app? (interesting in the context of PDFs which some people love, and others hate)

0 views
Unsung 3 weeks ago

Flickr’s optimistic committing

Somewhere next to optimistic loading and optimistic saving exists another technique to make apps feel faster: optimistic committing. Flickr is a great example. After navigating to photo upload, you enter a sort of a foyer where you can drag in the photos, reorder them, name and tag them, and otherwise prepare them before pressing the big Upload button. But Flickr also optimistically assumes you will press that button, and slowly starts uploading the heavy photos in the background the moment you drag them in. Like all optimistic schemes, being friendlier toward the user complicates things for Flickr’s designers and engineers. After all, there is still a regular upload modal after you do commit to the upload… = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/flickrs-optimistic-committing/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/flickrs-optimistic-committing/2.1600w.avif" type="image/avif"> …so the two states – quiet staging area upload, and the official visible upload – have to be reconciled and kept in sync. Also, optimistic but eventually cancelled uploads have to be cleaned up from the servers. Lastly, there’s signposting. Contrary to lighter optimistic loading schemes, which typically simplify reality by pretending no data transfer is actually happening, the optimistic committing here is actually visible through small indicators: I think this transparency is welcome. In the past, Meta (who else!) got into hot water for abusing optimistic committing : Did you ever record a video on Facebook to post directly to your friend’s wall, only to discard the take and film a new version? You may have thought those embarrassing draft versions were deleted, but Facebook kept a copy. The company is blaming it on a “bug” and swears that it’s going to delete those discarded videos now. They pinkie promise this time. In this context, it’s good that Flickr conveys data is being sent to the servers; I believe this helps with building trust. On top of transparency, I think it’s also good that this process shows the progress of uploading with a lot of precision – not just between files, but also within each file. Internet connection speeds vary so much, not just geographically, but also even situationally, that this is really helpful in practice. There are many moments where auto saving to the cloud needn’t bother the user unless the connection goes offline for a longer while, but this feels like a situation where clarity is better than magic. #details #loading states

0 views
Allen Pike 1 months ago

The Persistent Gravity of Cross Platform

This week’s discussion of the ChatGPT app and its move to Electron merits a link to my evergreen article The Persistent Gravity of Cross Platform : At the highest level, cross-platform UI technologies prioritize coordinated featurefulness over polished simplicity. I’ve added a coda to that article about how coding agents actually strengthen the argument for Electron on large teams, at least for now. The initial release of the new ChatGPT app has been clumsy – there’s a lot of work to do to get Electron ChatGPT (née Codex) as polished as it should be. But, like it or not, cross-platform code is the least-bad way to coordinate a massive team on a rapidly changing product.

0 views
Josh Comeau 1 months ago

Getting Started with Anchor Positioning

For decades, one of the most notoriously-challenging problems on the web has been sticking one element to another element, for things like tooltips and nested menus. The CSSWG has decided to provide a first-class solution to this problem, and it’s pretty friggin’ cool! In this tutorial, I’ll share the most useful parts I’ve found from this modern CSS feature.

0 views
Unsung 1 months ago

About Unsung: Recent improvements

(This is one of the meta posts about this very blog . If that’s not interesting to you, skip to the next one!) Here are some improvements I’ve made to Unsung in recent months. Always curious of your feedback or pointers to places that do these things better! Weekly emails. I made it so clicking on every (non-YouTube) video or image takes you to the equivalent of the weekly email you’re looking at, but on the web, where you can watch the videos in their natural habitat. It’s scrolled to the right position, so you can just continue reading there. I’m sorry, I know it isn’t great to shove people outside of their mailbox, but I don’t think there is any way for videos to work well inside emails, and a lot of Unsung is about precise videos. (The only thing allowed is GIFs, and they are really not up to the task.) Video playback. On that note, I improved the handling and controls of video playback. On mobile, you can tap to play/​pause and swipe left and right to move. On desktop, you can drag the handle, or also swipe left/​right. You can also use ← → keys to advance frame by frame. My goals are to have video controls that are both minimalistic (for example, never covering the contents) and precise, to match how videos are used here. (But if you tab to the video, it still shows “classic” controls for accessibility.) Blink comparators. You might have noticed that I added some blink comparators in a few posts where they seemed to be useful ( one , two , three , four ). Is that fun? Does it work for you? Because I have more ideas for light interactivity on Unsung. = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/about-unsung-recent-improvements/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/about-unsung-recent-improvements/2.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/about-unsung-recent-improvements/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/about-unsung-recent-improvements/3.1600w.avif" type="image/avif"> Technical details. Some people asked technical details about specific things on this blog, so I added a technical details page with answers. Dashboard. If you are interested in that kind of stuff, I added some more charts and stats to Unsung’s internal dashboard (and deprecated sentiment, which wasn’t really working). #about unsung

0 views
The Jolly Teapot 1 months ago

A peculiar bug in Safari

On weekend mornings, I have the inescapable habit of looking at my website and seeing what I can change, what I can remove, what I can improve in terms of HTML, CSS, layout, links, etc. This Saturday, as I wanted to look closer at the way the period at the end of a sentence rendered when appearing just after a word in italic (I know), I noticed something curious. When I zoomed in the page, using “Command – Plus Sign” (⌘+), I could see that the line length was changing with the size of the text. The bigger the text, the longer the line. You see, I’m very protective of the I use on this site —  — especially for Mac users, who see it in the Charter font. *1 This value sets an ideal number of characters for each line making it, when paired with the right line height, easier to read (supposedly). Zooming in on text shouldn’t change the line length, so I looked around and realised that I was a bit clueless when it comes to identifying bugs, and even checking if they were already reported. I found a few bug reports related to zooming in, but none of them described my issue. Not only that, but I didn’t really know if this was a Webkit problem, or a Safari problem. So instead of working my way to either confirming an existing bug or filing a new one , I did what I usually do when facing a problem: I avoided it altogether rather than trying to solve it. Therefore I changed to in my CSS, resulting in a similar line length for Charter. *2 With as the unit, zooming doesn’t modify the line length, so I’m pretty happy with this easy fix. Bonus point: takes up the same number of bytes as in my default CSS, still capped at 132 bytes. Imagine the extra-byte horror if I had to use something like or ? It would have ruined my sunny Saturday morning. This little website update made me realise something: my site design is pretty much done, and I hadn’t changed anything for a few weeks or even months. I actually miss the satisfaction of changing something at the end of my little routine. Checking every detail on every page, revisiting every line of code just to see what can be improved, even if it’s just removing extra quotation marks in an attribute or an optional closing tag, is not as fun when there is nothing to do at the end. I really like my site’s current design, and even if there might be a few tiny tweaks like this one in the future, I feel that the overall look and feel is pretty much final. It’s a weird feeling, but now I have no excuse for not writing more, and publishing more posts, even if they are unfinished , or shorter than usual . For others, falling back to the default serif, usually Times New Roman, is indeed a bit narrow; or would be better, but it’s too wide for Charter.  ^ For the serif/Times New Roman fallback, creates a slightly longer line, which is atually better than what it was with .  ^ For others, falling back to the default serif, usually Times New Roman, is indeed a bit narrow; or would be better, but it’s too wide for Charter.  ^ For the serif/Times New Roman fallback, creates a slightly longer line, which is atually better than what it was with .  ^

0 views
Unsung 1 months ago

Finder’s elite eliding

I know I’m usually driving the Finder pretty hard , but I think that’s a necessity, given its position as the center of macOS for power users, and its situation where it feels like Apple pretty much gave up on it. But I also want to show things that Finder does well, and this might be something no one does nearly as thoughtfully: text truncation. This is what happens when you have a filename that’s too long: This is really nicely done, for many reasons that work in lockstep: Why does this last thing matter? Because unnecessary tooltips are distracting, cover information, and also – maybe most importantly – turn the interface into a minefield where no safe places remain to just mindlessly rest your cursor without worry. This last thing is very fuzzy, but so important. You know how unpleasant a lot of articles are on the web these days, solely because you’re always on the edge about what’s going to happen while you read? Am I going to be moved up and down? When and where is the ad going to appear? When will I encounter a new subscription pop-up, and what will be the weird way to close it this time around? I know you don’t literally tense your muscles while reading those, but I feel like in some sense, in the back of your head, there is always this unpleasant worry that you’re dealing with an unstable interface . This is not a strong, but I feel a similar way about spurious tooltips; they make interfaces feel less stable. You rest your cursor, something jumps up at you, you get distracted and move your cursor instinctively to avoid it, and with any luck, you trigger yet another tooltip, and so on. I will write more about this in the future. If you asked my former coworkers, I bet a significant portion would say “this guy gets angry at tooltips, like, all the time.” I promise I will get angry at tooltips more here. But today? Today, kudos to the Finder. It shows us that if you care, you can make this small moment feel really great and thoughtful – knowing that small moments multiplied in the thousands are no longer small. #details #finder #interface design #mac os #typography Finder cleverly elides text from the middle, knowing that both the ending of the last words (or digits!) of the file name, and its extension are important. Finder shows the full name in a tooltip. I’m surprised how many tools forget to do that, offering no easy explanation for the missing letters. Here are some examples from Notion and Bear, neither of which offers help on hover: Finder position the tooltip exactly atop the existing text. I think this is really clever: it avoids overlapping other useful information, and makes it faster to reorient yourself. Compare with, for example, AirTable: Lastly, Finder only shows the tooltip when it’s needed . This is something where so many places lose their way. For example, here’s Paper and Google Drive, throwing up a tooltip indiscriminately, even if it has absolutely nothing to add to the conversation:

0 views
David Bushell 1 months ago

The modern app

Today I’m introducing the next generation of code editor. A modern app to satiate the needs of the discerning coder. We’re talkin’ blazing fast collaboration between man and machine . Try out the demo below (for best experience: desktop Chrome, obvs). If you’re reading this in RSS I have no clue what you’re about to see… maybe visit the demo it’s a fun one! Update ready (restart required) A modern app requires JavaScript, bro. Error loading documentation. Please disable your adblocker and try again. We and our 9172 partners value your personal data. You must accept the terms and conditions. Application error: a client-side exception has occurred (see the browser console for more information). Last edit: DELETED USER – 1 January 1970 – is this working? abandonment abbreviation aerodynamically antidisestablishmentarianism [Advertisement: 1% off subscription] an icon bar full of indecipherable icons with no label > Activate Windows Go to Settings to activate Windows. Fix the bug and make no mistake The user has asked me to fix his shitty code and to “make no mistake”… is he stupid? Thinking harder… On second review his code is garbage slop, should I search the internet and plagiarise ZA̡͊͠͝LGΌ ISͮ̂ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ Would you like to play a game? Thinking… Family photos were deleted to resolve low disk space error. iOS 26.6.9 is available, update now? Amazon driver is lost in your neighbourhood. Alice’s personal access token expired, switching to Bob’s. Tailwind language server crashed. SAMSUNG SMART REFRIDGERATOR ® has detected low milk levels: five gallons ordered. 418 I’m a teapot. close AI, AI, AI! We’ve heard you. There are now 26 new sparkle buttons! Can you find them all? Release notes dialog now has an embedded WSL 1.0 terminal emulator. It’s broken (issue: #25293). Reduced RAM usage when typing on the home row. Keystrokes are now logged in the correct Slack channel (fixes #7 and #933 through #980). root@localhost system32 C:\ $ _ Yeah so um… have you noticed that all modern software is teetering on the enshitty cliff? Everything in my dock is an Electron-ified enshittybomb one update from disaster. There used to be alternatives. Now those suck too. I don’t want to collaborate. How about you leave me alone and I’ll email you the file when I’m finished? Here, take a hard copy and jog on. You want to comment? I don’t remember asking for an opinion. Oh fantastic, now the computer thinks it’s people! I’ve got dialogs and popovers all up in my face yammering about agentic bollocks. Mystery icons everywhere. Wait… did they move my cheese? Ahhhhh! It’s all your fault! I sure as heck didn’t ask for it. Remember when they made entire video games on a 32 KB floppy disk? Those were real developers. v1 Release Notes: done. Can you stop adding new “features”, please? You had one good idea. Finish it already? Now you’ve got ten thousand GitHub issues. Well done. I used to enjoy making things on a computer :( Icons used: Griddy Icons MIT License. “Clippy” © Microsoft (this is parody). Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Today I’m introducing the next generation of code editor. A modern app to satiate the needs of the discerning coder. We’re talkin’ blazing fast collaboration between man and machine . Try out the demo below (for best experience: desktop Chrome, obvs). If you’re reading this in RSS I have no clue what you’re about to see… maybe visit the demo it’s a fun one! The Modern Editor Update ready (restart required) A modern app requires JavaScript, bro. Error loading documentation. Please disable your adblocker and try again. We and our 9172 partners value your personal data. You must accept the terms and conditions. Application error: a client-side exception has occurred (see the browser console for more information). Last edit: DELETED USER – 1 January 1970 – is this working? aardvark abandonment abbreviation aerodynamically antidisestablishmentarianism [Advertisement: 1% off subscription] Thinking… an icon bar full of indecipherable icons with no label > Activate Windows Go to Settings to activate Windows. Syntax errors: 3453 CI warnings: 6462 Merge conflicts: 1130 Tokens maxxed: 9512 Logged in as: ghp_nD7FQLmQlmaoRis27Lq2C69HWTFwsU420CvL Fix the bug and make no mistake The user has asked me to fix his shitty code and to “make no mistake”… is he stupid? Thinking… Thinking harder… On second review his code is garbage slop, should I search the internet and plagiarise ZA̡͊͠͝LGΌ ISͮ̂ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ Thinking… Would you like to play a game? Running NPM post-install scripts. Claude is not in the sudoers file. This incident will be reported. Windows will restart in 5 minutes. Production database was dropped. GitHub connection timed out. Incoming phone call from your mother. CI/CD deployment failed again. Family photos were deleted to resolve low disk space error. iOS 26.6.9 is available, update now? Amazon driver is lost in your neighbourhood. Alice’s personal access token expired, switching to Bob’s. Tailwind language server crashed. SAMSUNG SMART REFRIDGERATOR ® has detected low milk levels: five gallons ordered. 418 I’m a teapot.

0 views
Unsung 1 months ago

“Invalid-reverse-solidus validation error”

In my three decades online, it has never occurred for me to try this, and I found it so delightful once I did – both Chrome and Firefox will quietly rewrite backslashes in URLs into slashes: Not Safari, however, even though the URL living standard says it should . I am very curious if the presence of backslashes in URLs is owing to Windows still showing backslashes in file paths, or just because people casually don’t see any difference between / and \, which are arguably both similar, and relatively alien in everyday typography. (“Solidus” is the proper typograpical name for this kind of a slash, partly to disambiguate it from all the other slashes with their equally fascinating names .) = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/invalid-reverse-solidus-validation-error/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/invalid-reverse-solidus-validation-error/2.1600w.avif" type="image/avif"> #keyboard #typography #web

0 views
Evan Schwartz 1 months ago

Scour - June Update

Hi friends, Many of you mistakenly got onboarding emails yesterday. I'm sorry about that. I was tweaking the way emails are sent to new users and accidentally sent it out to everyone. Don't worry, you'll get your weekly digest on Friday as usual. (If you got a message about verifying your email, please do verify yours if you'd like to continue receiving the weekly digests.) In June, Scour scoured 841,977 articles from 27,356 feeds , and 123 new users signed up. Welcome! Here's what's new in the product: Scour now tracks and shows which articles cover other ones so you can find coverage, reactions, and responses to a given story. Under any post, you can see both the stories that the given one links to, and which other sources link to it. A detail I especially like is that the covering sources you tend to like and read are shown first, so you can easily find your favorite commentators' reactions. Relatedly, there's now a page that shows the most widely covered stories across Scour. If you subscribe to specific feeds, you can also add this as a feed to source content from. Laurynas Keturakis suggested this over a year ago and after finally implementing it this month, it quickly became one of my favorite Scour features. Thanks Laurynas! After you love or like a post, you'll see a small prompt to add more interests similar to that article's content. Adding interests is the best way to hone your feed and make sure Scour surfaces articles you'll like, so I hope this makes it easier to do that. If you subscribe to individual feeds, that prompt will also include a way to subscribe to the publisher's feed, if you aren't already, so you'll get more content from them. Similarly, if you dislike a post, you'll see some options to have less of that kind of content appear in the future. The Scour feed got a makeover! The new layout should be easier to scan and interact with. Clicking or tapping a post opens the expanded view: Also, on mobile, you can swipe articles right or left to quickly like or dislike them. The new Discover section contains all of your personalized interest and feed recommendations, as well as the pages to browse popular posts, interests, and feeds. Head over there if you'd like to build out your feed more, or if you want to see what others are reading on Scour. Scour now works far better with assistive technology. Every post is a labeled article whose actions are reachable by screen reader and keyboard, menus support arrow-key navigation, and the things that used to change silently (filter updates, search results, newly loaded posts) are announced as they happen. If you or someone you know reads Scour with assistive tech, I'd love your feedback. See the new Accessibility page for the full picture. Enjoying Scour? I added testimonials to the homepage and I'd love to include your review! Email me to let me know your thoughts (and of course, constructive feedback is also very welcome). Here were some of my favorite articles I found on Scour in June: Happy Scouring! I've been thinking a lot about the ways that AI changes what it feels like to be a software engineer and I especially appreciated these takes: Andrew Diamond made a great comparison with historical fiction writers in Software Engineering in the Age of AI . Vardan Torosyan pointed out that every engineer is now facing the kind of overload engineering managers have always dealt with: There is Too Much . Candost discusses having an ownership mindset in On the Changing Role of Software Engineers . And a goofy font that Bill Tarbell made that's readable for humans but not for AI: Souls Only .

1 views
Unsung 1 months ago

¿Por qué no los dos? pt. 1

I praised ⌘⭲ recently in my essay for cleverly not showing itself when you press the keys really fast . Here’s another nice detail. If you press and hold ⌘⭲, you will eventually stop at the end. (You can then press ⌘⇧⭲ or ⌘` to get back.) However, if you are already at the end, pressing ⌘⭲ again wraps around to the beginning: The issue of whether to wrap around or not is more universal; you can see it in many lists, ⌘F, and so on. On one hand, it’s nice to have a solid deterministic end that you can rely on stopping at, especially since sometimes the last item on the list is special (“See more items…”). On the other hand, going all the way back from the end can be frustrating, too, especially on a Mac that does really strange things with Home/End/PgUp/​PgDn keys. I thought the hybrid approach that ⌘⭲ is doing here was clever, and might be applicable elsewhere. #flow #keyboard #mac os

0 views
David Bushell 1 months ago

ARIA, anti-patterns, and you

Please take a minute to understand what ARIA is and is not. ARIA and especially the ARIA Authoring Practices Guide (APG) are commonly misunderstood. I read an article the other day that had this facepalm moment: And with modern LLM agents, turning a spec into working code is surprisingly fast. Point the agent at the APG pattern, describe your component’s markup, and get a solid first draft you can refine and test. This is worrying, and the use of “LLM agents” isn’t the worst part! The APG is not a how-to guide of ‘best practices’ for building accessible websites. It exists to demonstrate how the ARIA specification should work in theory — regardless of support and regardless of whether more accessible, non-ARIA patterns exist (they do). As Eric Bailey notes — The guide was originally authored to help demonstrate ARIA’s capabilities. As a result, its code examples near-exclusively, overwhelmingly, and disproportionately favor ARIA. What I Wish Someone Told Me When I Was Getting Into ARIA - Eric Bailey — which makes sense, because: Browser and assistive technology developers can thus utilize code in this guide to help assess the quality of their support for ARIA 1.2. Read Me First - ARIA Authoring Practices Guide (APG) Even if ARIA was fully supported ( it’s not ) the APG still wouldn’t be a ‘best practice’ guide. ‘Best practice’ is not using ARIA at all. If you can use a native HTML element or attribute with the semantics and behavior you require already built in , instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so . 2.1 First Rule of ARIA Use - Using ARIA, W3C APG exists in a vacuum to show off the ARIA spec. The button example includes this code, for crying out loud! I’m unaware of any circumstance where should ever be used over a . Before you tell me you can’t edit your React component library, do the web a favour and delete your codebase. In fairness, the button example has a “Read This First” disclosure — and guess what: they use a element and not the disclosure pattern because the APG isn’t best practice. It’s hard to blame developers for misusing ARIA and the APG. I’ve been confused myself. As W3C documentation goes, APG is rather sexy. It’s a useful resource if you understand why it exists. Misuse of ARIA has made the web less accessible. Increased ARIA usage on pages was associated with higher detected errors. The more ARIA attributes that were present, the more detected accessibility errors could be expected. The WebAIM Million - WebAIM Avoid ARIA where ever possible. Don’t point a freaking LLM at the APG! I can’t believe I’m saying this but use Google’s slop if you absolutely refuse to learn/code yourself. Apparently OpenAI is throwing ARIA at the web and seeing what sticks. Ahhh! I don’t know anymore, take some pride in your expertise? P.S. name an assistive technology that isn’t a screen reader. Ain’t easy, is it? So don’t be casually punctuating with the word “test” like it’s some get-out-of-jail-free card for your dubious practice and advice. “Overview of Digital Accessibility Technologies” by Declan Chidlow is a great help if you want to win this game at parties. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Unsung 1 months ago

Fingers already on the keyboard

This is what happens when you go to the homepage of Gemini and start typing quickly: Mechanically, I think this is React or some other framework setting focus again with some delay, but the end result is… rather disturbing. While the technical solution would be to fix the problem or at least do not set focus again if already set, I wonder what’s the real challenge here. I imagine it might be that the testing process (if any) assumes using the mouse or trackpad first. In this case, moving the hand to the keyboard to start typing gives the interaction just enough delay to miss the second, unnecessary focus. I think a good assumption to have for all common interactions is that for some users, fingers are already on the keyboard and things can happen so much more faster than you expect. Not accounting for that, the creators of this flow inadvertently broke one of the cardinal rules. We talked about it in the context of mouse pointers before, but it applies as well to text: don’t move my cursor for me . #flow #keyboard

0 views
Unsung 1 months ago

“That knowledge slides away.”

In response to my recent interactive essay about interactions , Waider on Mastodon posted a great crystallization of a common problem: There is nothing quite so frustrating as a persistent user interface papercut. You know it’s there, but you keep running into it because the moment you start thinking about what you’re doing instead of how you’re doing it , that knowledge slides away until BAM you run into it again . I think this is really nicely put and highlights about why it’s very important to care about this kind of stuff. If you forgo a standard interaction out of carelessness, a bug, bad systems thinking, or for other reasons, you’re not just making your users frustrated by something not working. You’re also at risk of making them frustrated at themselves , assuming they can change what their fingers do easily, not fully knowing that a) this is motor memory, not just regular conscious actions (and any memory is hard to “update” intentionally), and b) motor memory is separated from regular, declarative memory, and not possible to reason with using the same techniques. (As an example, it’s very hard when keyboard shortcuts or mouse gestures disagree between apps, because while you consciously might know which app you’re in, that’s not necessarily true of your fingers.) Waider continues with an example: The canonical example of this, for me, is Microsoft apps on macOS: even now, decades after Microsoft started producing macOS versions of their apps, they insist on largely disregarding the native UI idioms in favour of their own. Current pet hate is that if I’m commenting on a document, the Ctrl-A/Ctrl-E actions do not work, and boy howdy do I use those constantly. My recent example is that even though I wrote about Safari overriding the natural “scroll to top/bottom” tap gesture on their tabs – so I am aware of it in my declarative memory, I know Safari designers messed it up, and I know exactly what to do and not do – my fingers still occasionally tap to scroll in Safari anyway. #details #flow #interface design

0 views