Posts in Css (20 found)
Lea Verou 4 days ago

Dark mode toggles: two states are enough

A good two-state toggle can actually express all three data model states. Until recently, if you looked at most websites with a theme toggle [1] , you’d find three options: Light , Dark , and System . Examples of tri-state dark mode toggles. In (LTR) reading direction: Ant Design, Red Hat Design System, Web Awesome, Excalidraw, Taiga, Astro, Hero UI. Thankfully, these days the trend has shifted towards a simpler two-state toggle, but tri-state ones are still incredibly common. Examples of two-state dark mode toggles. In (LTR) reading direction: Vitepress, Material Design, Adobe Spectrum, Radix, ShadCN. The rationale sounds plausible: “System” is a different intent than “Light” or “Dark”! One is a policy ( whatever my OS says, do that ) The other is a value ( dark, forever, I don’t care what my OS says. ) Surely, users should be able to express that intent! Except, real users don’t generally seek out dark mode toggles to express intent for things to stay as they are, they seek them out when things need to change. Think of the user goal when browsing a website (as opposed to a separate Settings page, where three states are fine ). E.g. on a documentation site, they may be there to look something up. On a landing page, they may be trying to evaluate whether the product is suitable for their needs. On a media site, they may be there to read the news. On a graphics app, they want to draw something. One thing is for certain: tweaking the theme is not their primary goal [2] . To get in the mindset of tweaking the theme, something needs to be off . When things look right, users just move on with their actual goal instead of thinking about the theme. The tri-state control is solving a largely imaginary user goal that is extremely rare among real users, and does not justify the additional complication and UX friction of a three-state toggle. Worse, it forces the user to decide between choices that produce no visible difference, breaking the principle of feedback . Yes, tri-state toggles are common . That doesn’t make them good . This essay explains why, and how to do better. One of the most common UX mistakes is designing UI around the underlying data model instead of user goals. Good interfaces abstract away the underlying model and expose a model that aligns with user goals (unless of course these happen to coincide, which is rare). This is exactly the case with tri-state dark mode toggles; exposing all three states is data model leaking into the UI. Yes, there should absolutely be three states in the underlying implementation! But at any given point, one of them is irrelevant to the end-user. Users cannot meaningfully express intent about problems they don’t currently have. A dark mode toggle is a temporary comfort adjustment . When it comes to user goals, there are only two real states: You’re reading in bed, the page is a flashbang, you hit the toggle. You’re on a laptop outside and the dark theme is unreadable in sunlight, you hit the toggle. It’s situational, it’s immediate, and it’s usually about the environment you’re in rather than a considered long-term stance on color schemes. A third state assumes a usage scenario where a user visits a website that looks perfectly fine, and still looks for a dark mode toggle to ensure it can continue to look fine in the future. Users do all sorts of weird things, so I won’t assert that this never happens, but it is not a natural user interaction, fueled by a real user goal. Even the strongest proponents of tri-state toggles I have spoken with either admit they have never done this, or bring up some extremely rare, weird one-off edge cases. I tried to ask on social media ( Bsky , Twitter/X , Mastodon , GitHub ), but no matter how hard I tried to word it well, the question kept getting so misunderstood that the data is too noisy to be useful. Besides people misunderstanding the question and talking about these times where they want to override the OS theme, there were also developers answering about debugging use cases (rather than their actual user behavior), or people talking about that one time they accidentally clicked that option. There was even someone who concluded that because they want the site to inherit the OS setting almost always, they should vote “frequently”! 😵‍💫 That said, I don’t think it matters all that much beyond academic curiosity, as a good two state control can actually express all three states — users just need to apply the override the first time it becomes relevant. One could argue that sure, the third state is not frequently needed, but surely it doesn’t hurt to have it there for the one user that will need it, right? But a more complex UI has a cost. It increases cognitive load for interacting with the control and forces you towards certain UI design decisions. A two-state toggle can be very compact: Just a single icon that switches to another when clicked. Some websites do go that route with a tri-state toggle that cycles through three states. Docusaurus for example: But generally, the ergonomics of that are poorer than for the two state toggle, so it is no surprise it’s rare (Docusaurus was the only example I could find). Some tri-state controls go for three icons side by side, which triples the screen real estate used. Others, in an attempt to balance clarity and real estate, resort to a dropdown: That improves learnability, at the cost of efficiency, as it turns a single click interaction into a two-step process. The actual perceived friction is actually worse than one extra click. Perceived friction is not a pure function of user actions, but also of the mental effort required to make a decision, and larger UI shifts (e.g. opening a dropdown) are more cognitively expensive than smaller ones (e.g. clicking a toggle) as the user needs to perceive and interpret a larger area. Guidance towards using tri-state controls is well meaning, but often based on paring good tri-state controls against poor two-state ones. E.g. in this article by Bramus : Above that many implementations I have seen don’t take the “System” value into account. By omitting this option, the sites will never be able to respond to the system preference again, as they always have an override applied. Indeed, a bad two state toggle is worse than a tri-state one. It makes the system mode unreachable once tweaked, making the selection irreversible and violating the usability principle of user control and freedom . A good two-state should be able to express all three states. The idea is that the underlying model is still three states, but only two are shown at any given time : When you press it for the first time, it toggles to the opposite of what you’re currently seeing, and stores the literal value ( or ). The next time you press it, it toggles back to the system default , and removes the stored value. That last bit is the one many two-state toggles get wrong. Storing a value that happens to match the system preference silently converts a temporary adjustment into a permanent pin with no way out. Another common mistake is being overzealous about removing the stored value when the system preference changes, even if the user has explicitly set an override. This evaluation must only happen at user interaction. This is important because many users have their OS set to automatically switch between light and dark mode based on time of day, and removing the stored value proactively would make it impossible for them to actually pin a theme. If a stored override later happens to coincide with the system preference — because the OS changed, not because the user did anything — you keep it . This looks like an oversight — they’re the same now, why not tidy up? Because tidying up silently downgrades an explicit choice into a default, based on an event the user didn’t cause and can’t see. Here’s a concrete scenario that you can navigate interactively ( view on separate page ): An argument I heard when discussing this was “but if the user selects light when their OS is light, then the OS switches to dark, won’t they get confused that the website did not preserve their choice?” People hypothesizing that other people, who are not them, will get “confused” is a bit of a pet peeve of mine in usability discussions, but let’s entertain it for a moment. Here’s that exact scenario: Remember, this control is entirely tangential to the actual user goal for visiting the website. Even if their intent were to pin light instead of reverting to System (light) , this is something they would only notice once these diverge, i.e. the OS switches to dark. At that point, fixing it is a single click away. It’s such an easy fix, that there is no point in dwelling on it further. It’s not that this never comes up, but making the tradeoff in favor of a tri-state control isn’t justifiable, IMO. A tri-state control introduces permanent UI complexity to prevent a one-time, easily fixable problem . Additionally, color appearance is not just a pure function of color components, but also affected by surroundings and other factors. Even if a website implements only two modes, light mode may look slightly different in a light OS vs a dark OS, so selecting it as an override makes it an informed decision . The title and icon could make the state clearer (e.g. the tooltip saying “Switch back to light (system default)” instead of “Switch to light” or the icon having a small screen icon instead of just a sun or moon). But those would need user testing to validate that they are an actual improvement. My concern is that once you distinguish System (light) from light , it (ironically) could become the thing that primes users to seek a third state that they previously had not considered. Even if there is an ingenious UI that exposes three states at the same time without adding any cognitive load or friction (I have some ideas about what that might look like), I’m unconvinced this is a problem worth solving, and feels a lot like the UX version of premature optimization . Although I spent the whole article arguing against tri-state toggles, there are actually valid use cases for them. These are the two cases I’m aware of, but feel free to recommend more in the comments! This article is primarily geared towards a permanently visible toggle in the header or footer . A setting that lives alongside other settings in a settings panel is a fundamentally different usage scenario: It is no accident that while 2-state toggles are becoming the norm for persistent controls, tri-state is (rightly) king for settings panels. Bluesky’s Appearance settings panel. The tri-state is fine here. Showing the “Dark mode” option below even when it produces no effect, on the other hand… Google Calendar. Love the icons, it would be nice to actually indicate what System currently resolves to. I’m not one to praise post-X Twitter, but having two two-state toggles instead of one tri-state is a very interesting design choice. The UX is not quite there, but if done well, I think it could be the best of both worlds when you have the screen real estate. This entire essay assumes the common case where a website only has two color schemes: light and dark, and there is no difference between light mode in a dark OS vs light mode in a light OS. Vadim Makeev had an interesting idea : color schemes should take the underlying OS setting into account. Light mode should be less bright in a dark OS and dark mode should be less dark in a light OS, to reduce the contrast between the website and the rest of the system. I have not seen many UIs doing this, and CSS does not make it easier ( is very much designed around duality), but if you are actually doing this, you have earned your three states my friend , display them as prominently as you like, none of this applies to you! Edit: I reached out to Vadim to ask if he had seen any UIs following his guidance. Here’s what he had to say: Unfortunately, I haven’t seen any websites using this idea. I would say we’re pretty limited with tools currently to do so. The moment we want to override prefer-color-scheme, the whole light-dark() convenience is falling apart. Yet another problem that CSS functions will solve (nothing preventing us from creating a 2-4 arg version of this ). The dark mode toggle is a nice case study, but the underlying lesson is bigger: Users do not seek out solutions to problems they don’t currently have. The tri-state toggle is the GUI version of low signal-to-noise APIs that ask you to pass dozens of parameters that could have sensible defaults, forcing you to decide on problems you have not encountered and are not relevant. Do not flood users with options that are irrelevant to their current situation. Options that might become relevant in the future, should be surfaced in that future, not pre-emptively. Not every state of your state machine warrants visible UI. Ultimately, everything boils down to the very same principle: Respect user effort. Thanks to Chris Lilley and Jake Archibald for reviewing an earlier version of this draft Unless otherwise noted, this refers to a permanently visible toggle in the header or (rarely) footer, not a theme setting in a separate settings panel. ↩︎ This is about users. Yes, the developers of the site may have a goal of testing the theme, but we optimize UIs for being used , not getting debugged. ↩︎ The website looks ok. The user moves on with their actual goal and doesn’t look for the toggle at all. The website is too bright or too dark to be comfortable. The user wants to fix it. Your OS is in light mode and the site has stored nothing, so the page follows along. Flip the OS control to run this the other way round. You toggle. The target is dark , which is not what the OS says, so the site stores an override. The page goes dark . Your OS switches to dark . The override now matches it but is still kept . Nothing visibly happens, which is correct. Your OS switches back to light . The page stays dark , because the override is still active. You toggle. The target is light , which is what the OS says, so the override is removed . The page follows the OS again. Your turn. Both controls are live and nothing from here on is scripted. Drive them in any order and watch what does — and does not — end up in . Your OS is in light mode and nothing is stored. You toggle to dark , which is stored as an override. You toggle again, meaning to pin light . It matches the OS, so the override is removed — you actually got the system default. Your OS switches to dark and the page follows . Not what you meant! But the fix is a single click: light no longer matches the OS, so this time it is an override, and thus pinned, so this can only happen at most once . The user is already in the mode of making decisions about their future The expectation is not that every setting must produce immediate feedback There is a lot more screen real estate to explain three states. Unless otherwise noted, this refers to a permanently visible toggle in the header or (rarely) footer, not a theme setting in a separate settings panel. ↩︎ This is about users. Yes, the developers of the site may have a goal of testing the theme, but we optimize UIs for being used , not getting debugged. ↩︎

0 views
Simon Willison 4 days ago

One-shotting a Raccoon Heist game using Claude Fable 5

Back in 2024 I tweeted screenshots of a game concept generated by GPT-3 and some concept "art" created using DALL-E. Today, on the fourth anniversary of that tweet, I decided to see if Claude Fable 5 (running in Claude Code for web ) could build the entire game from the content of that tweet. It did a pretty good job of it! You can play the game here . Here's the GitHub repo , and a short video demo: Your browser does not support HTML5 video. This is the August 5th, 2022 tweet : My GPT-3 prompt back then was: This was in the text completion days, where you asked GPT-3 to complete the rest of the text. It completed: In "Raccoon Heist", you and your team of thieving raccoons are tasked with pulling off a series of daring heists. From robbing banks to stealing priceless art, no job is too big or too small for your furry crew. You'll need to use your wits and your skills to avoid the police and make a clean getaway with the loot. With exciting gameplay and a charming cast of characters, "Raccoon Heist" is the perfect game for anyone looking for a light-hearted caper. My prompt to DALL-E was short: Today's experiment: can I dump those screenshots into Fable 5 with a prompt telling it to write a game, then leave it to its own devices and get a working game at the end? A frustrating thing about Claude Code for web is that it can be hard to test what it's working on while it's still working. I've been using GitHub Pages to work around that limitation, and found it to work really well. Here's my process: That's all it takes! Within about 30 seconds of each push the latest content will be visible at . If you do this with a private repo, anyone who can guess the name of the repo will be able to view the published content. I don't worry much about this myself. Here's the prompt I gave Fable 5 (written in the notes app on my phone - this entire project was conducted on mobile). I accompanied it with the two images from the original tweet. I didn't make any technology choices. I assumed (correctly) that it would probably use Three.js based on previous experiments. Giving Claude access to an OpenAI key turns out to work really well for filling in gaps in its capabilities - in this case we needed some way to generate images to use as textures. Fable is very good at prompting image generators! I said "Work independently - do not ask me to make any further design decisions" because I wanted to see if it could produce a full, working game without any further input from me. I also said "Commit and push as often as possible so I can preview your work". When you use Claude Code in the Claude iPhone app you give it a GitHub repository and it works in a branch. Telling it to "push as often as possible" means commits start landing in that branch straight away. I like asking for as a bit of added flavor - here's that finished file , and the entry it made when it added the dog: New escalation: from night 3 the yards get a patrolling guard dog — a low-poly brown hound with a spiked red collar and a wagging tail. It wanders between random spots, and within 12 units it catches your scent and tracks you by smell (line of sight is irrelevant — it's all nose, shown by a 👃 over its head and barking). It gives up if you open a 17-unit gap. Getting caught messages are now source-specific: guard / headlights / hound. Verified wander → track → caught with an automated test. You can access the Claude Code shared session , and I also used my claude-code-transcripts tool to export my own HTML version which you can find here . Fable started with an index page, vendored a copy of Three.js, then wrote its own gen_textures.py script ( copy here ). It generated the textures and spot-checked them to make sure they looked OK. The metal.jpg file it generated for the trash can looks like this, though I don't think it was applied exactly right in the game itself: The raccoon, dumpster hideout, and both crew raccoons are now perfectly visible on mobile. Committing this critical fix. It decided to generate a title screen, which it did using this gen_title.py script. Here's the prompt it used for that: And the resulting image (which Claude thought was "gorgeous" ) - though I note that when it's shown on desktop it gets cropped to just the top third without the raccoon! Then my favorite change: it added the dog : And did a round of testing on it using Playwright, including another screenshot . It made a few more tweaks, then declared itself done : Raccoon Heist is built, tested, and pushed — 7 commits on , all verified with automated Playwright playthroughs on desktop, portrait-phone, and landscape-phone viewports. The game: You're a masked raccoon sneaking out of your crew's dumpster hideout into a moonlit cul-de-sac. Grab coins, gems, rings, donuts, and fish (they visibly stack on your back and slow you down), then waddle home and bank them before dawn. Each night raises the goal and the heat: night 1 has one flashlight-sweeping guard, night 2 has two, night 3 adds a scent-tracking hound that ignores line-of-sight entirely. A police cruiser sweeps the street with headlights, homeowners randomly peek out of windows, a seagull periodically dive-bombs your unclaimed loot, pizza slices trigger a 6-second speed FRENZY, and a rare golden TV is worth 120 points if you can haul its weight home. Win ranks: TRASH PANDA ⭐ → CAT BURGLAR ⭐⭐ → MASTER OF DISGUISE ⭐⭐⭐, with your best run saved to localStorage. How it's built: Vendored Three.js (no CDN), procedural low-poly characters, and a procedural WebAudio soundtrack (sneaky walking-bass jazz plus all sound effects — zero audio files). All seven ground/wall textures and the title-screen hero art were generated with gpt-image-2 and committed as static assets, so the deployed game makes no API calls. Mobile gets a dynamic touch joystick (appears wherever you touch) plus a DASH button; desktop uses WASD/arrows and Space. Testing caught two real bugs worth mentioning: the canvas rendered at 2× size on any real phone (a assignment was wiping Three.js's inline sizing — desktop DPR-1 tests masked it completely), and the win screen's star-rating div inherited the title screen's full-screen CSS and silently swallowed every tap on the "next night" button. Both are fixed and regression-tested, along with pickup, banking, chase/caught, dawn, night progression, and lose/retry flows. has the full build log. The game is live on the branch — just serve the repo root and open . It's an impressive starting point, but it's not a good game. You move your raccoon around a yard collecting items - donuts, fish, gold coins, jewels - while avoiding guards with flashlights and, in later levels, a dog. You have a limited carrying capacity, and once that's full you need to drop stuff off at the dumpster. If you pick up a pizza slice you get a temporary speed boost. There are no team mechanics at all - there are two other static raccoons next to the dumpster but they're purely decoration. It gets slightly more challenging as the levels progress - the dog introduced in level 3 is the most interesting new mechanic - but it's very, very easy to beat. It's also pretty boring - each night has a fixed duration and you can collect all of the items and then have nothing else to do while waiting for the dawn. I was impressed by the implementation. It's fully 3D, there are trash cans, the flashlight illumination cones are fun, and it has a reasonably coherent visual style. It works on mobile. The music ("a procedural WebAudio soundtrack (sneaky walking-bass jazz plus all sound effects — zero audio files)" according to Claude) is simple but feels about right. As a finished game project, it's mediocre. As a starting point from a single prompt I think it's very impressive. I've vibe coded up quite a few games now. They've all been deeply disappointing from a gameplay perspective - it turns out designing games that are fun remains a uniquely human trait, and one which requires significantly more skill and experience than either Claude or I can bring to bear. That said, I thoroughly recommend tinkering with game development projects as a way to explore the capabilities of agents. It's a fun, low-risk way to try out new things. If you stick at it long enough you might even produce something that's worth playing! You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Create a new repository for the project at https://github.com/new - this can be public or private, the trick works equally well for both. Start a Claude Code for web session, in the Claude iPhone or Desktop apps or in the browser at https://claude.ai/code Tell Claude what to work on, and encourage it to commit an page as quickly as possible. This will create a branch with a name like Navigate to the Settings -> Pages area for the repository ( in my case), select "Deploy from a branch", pick the branch name, and hit Save.

0 views
Justin Duke 1 weeks ago

Cursed knowledge

Nick pointed me towards Marcin who pointed me towards immich's list of cursed knowledge the other day, and it has already become a running joke in the Slack. Here is a baker's dozen of Buttondown's own cursed knowledge: 1 Yes, that's the joke. The Python library assigns the device family to every non-Mac desktop browser The HTML attribute only filters what the file-picker dialog shows you; drag-and-drop and clipboard paste bypass it entirely. Safari and Chrome re-serialize quoted CSS custom-property strings differently when you read them back via : Chrome keeps the single quotes, WebKit rewrites them to double quotes. Django emits a — which fails our CI — for any cache key over 250 bytes or containing a space or control character. Python's has no default timeout and will, given the opportunity, wait forever. SPF directives recursively chain DNS lookups against a hard cap of ten — exceed it and you get a , which can fail authentication for all of your mail. Outlook and Hotmail enforce mandatory TLS but serve a certificate chain rooting at DigiCert Global Root CA (G1) — a root that Ubuntu has since removed from its trust store. Django's tests whether the key exists , not whether its value is JSON . does not lock rows in the order you listed them — Postgres locks them in executor scan order, which is a wonderful way to deadlock two queries that both thought they were being careful. A postgres cannot exceed ~1MB. Stripe will send subscription update events for paused subscriptions. The Python library assigns the device family to every non-Mac desktop browser The HTML attribute only filters what the file-picker dialog shows you; drag-and-drop and clipboard paste bypass it entirely. Safari and Chrome re-serialize quoted CSS custom-property strings differently when you read them back via : Chrome keeps the single quotes, WebKit rewrites them to double quotes. Django emits a — which fails our CI — for any cache key over 250 bytes or containing a space or control character. Python's has no default timeout and will, given the opportunity, wait forever. SPF directives recursively chain DNS lookups against a hard cap of ten — exceed it and you get a , which can fail authentication for all of your mail. Outlook and Hotmail enforce mandatory TLS but serve a certificate chain rooting at DigiCert Global Root CA (G1) — a root that Ubuntu has since removed from its trust store. Django's tests whether the key exists , not whether its value is JSON . does not lock rows in the order you listed them — Postgres locks them in executor scan order, which is a wonderful way to deadlock two queries that both thought they were being careful. A postgres cannot exceed ~1MB. Stripe will send subscription update events for paused subscriptions.

0 views
Unsung 1 weeks ago

In between the lines

Over the years, I acquired this weird collection of almost-invisible, but important signifiers of when I know a product really focuses on craft and thinks about its users. I thought about one recently. Here’s what happens when you try to copy a long block of text from YouTube’s (otherwise very useful) text transcript pane: And here’s an analogous example from GitHub: GitHub’s arrives ready to go. YouTube’s throws in a lot of messy things in between the lines. Why does it matter? Because these both feel like places you’ll be copying a lot from, and dealing with a messy paste can feel so, so unpleasant. You have probably seen this chart before, from xkcd : = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/in-between-the-lines/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/in-between-the-lines/3.1600w.avif" type="image/avif"> This is the fabled automation trade-off, or the high fixed cost vs. low variable cost dilemma. Yeah, if you’re doing a lot of copy/​paste, you might invest in creating some sort of a clean-up step, or even going through a programming text editor which has multiple cursors or other casual automation . But what if you don’t do that often, or if you don’t even know how much time it’d take you to automate it? Then the investment seems scary or insurmountable, and you’re stuck doing something like this, time and again: And it’s really nice to encounter a place like GitHub, where the team was thoughtful enough to save you all this trouble. There is also an asymmetry that’s worth pointing out. I believe making this good doesn’t have to be a lot of work for people putting these surfaces together. Here’s me fixing the YouTube situation with two simple lines of CSS with : I don’t know if it’d be as easy for all big text block situations, but I think it’s good practice to look around a bit and think about what are tiny things that you can do on your side that will save your users minutes or hours of tedium (see also: recents and paste and even more recents ). #copy paste #selection #text editing

0 views
Farid Zakaria 2 weeks ago

Small site changes and new design

I have been wanting to refresh my site for a while now. I have even posted a few times on LinkedIn and X offering to pay if somoene was interested in taking a stab at it. I am sure this will put people off from reading my blog just stating this, but I decided to take a stab at some design changes with AI and some minor site improvements that I have always wanted to do. If you think the site looks much worse, please let me know . Some small improvements and quality-of-life enhacements: Look at me! I’m sidebar content I will undoubtedly abuse. I continued to try and give it my personal flair to avoid it looking like AI-slop but 🤷 A small entry in the footer to showcase the site is built with Nix. A Jekyll plugin that automatically creates source-sets for the images I use in my blog posts. A Jekyll plugin to create a short content hash for the stylesheets to avoid the browsers caching stale CSS. A Jekyll plugin that generates a little curved line for the masthead that is the of the page. Redesigned the layout to allow side-bar content.

0 views
マリウス 2 weeks ago

A GTK4 ssh-askpass in Zig

I run hardened Gentoo on my laptop, and most of the time I never touch because I’m using keys for most of the systems. There is one class of situation where I do need it, though, which is when a program wants an SSH key passphrase for a regular ED25519 key, but has no terminal to read it from. The usual case is , or the toolchain in general, fetching a private module over SSH during a build that runs without a TTY. OpenSSH can’t prompt on a pipe, so it runs whatever points at and puts the passphrase prompt in a window instead. For years I had nothing installed for that and had to work around these scenarios. The main reason for that is what Gentoo ’s Portage offers: Each of these has at least one inconvenience I didn’t feel like putting up with. My system runs with the global USE flag, so anything that needs X11 is out before I look any further. Of the five, is the only one with no X11 dependency whatsoever, which should have made it the obvious pick, but the trouble is everything else that comes with it. As a Sway user , I did not want a full KDE stack on the machine just to type the occasional passphrase, and that is what a install pulls in: is next, and it needs outright. On top of that it pulls in a few KDE framework packages and a Qt built with support, which collides with the already on my system that was compiled , so Portage stops on a slot conflict: needs as well, this time by way of GTK2 and a Cairo built with support: is X11 by name, so no surprise there, and it also needs the old imake build system, namely and , to compile at all: That left . At first glance it looked like the one option that needed no at all, but that turned out to be wrong. It does need X11 , and the ebuild appears to be broken about it. The build calls and the source includes , an -only GDK header, so on a system compiled without it fails to build: This is where I gave up on the packaged options. Even setting the X11 question aside, every one of these uses GTK2 or GTK3 at most. However, it just so happened that I had wanted to build something with GTK4 for a long time, so instead of patching one of the existing implementations, which are mostly C anyway, I wrote my own with Zig 0.16 and GTK4 , and called it ssh-askpass-zigtk . The reason the GTK helpers break on my system is the headers. The standard way of calling GTK includes the GTK4 headers, which pull in GDK , and GDK still ships on most installs, so an X11 header comes in whether you want it or not. Zig ’s , the obvious way to call a C library, would do the same, because it pulls in exactly those headers. So doesn’t anything. declares the thirty-odd GTK and GLib functions the program calls by hand, as plain prototypes: Nothing in that file names a symbol from or , so the compiler never sees an header, and the binary builds and runs against a GTK4 that was compiled without X11 . The one -adjacent value it needs, the Escape keysym, is hardcoded as rather than pulled from . GTK is built on GObject , which does single inheritance by putting the parent struct as the first member of the child, so a window, a box, a label, a password entry and a button are all layout-compatible with a at the ABI boundary. On the Zig side one type stands in for all of them, and every widget function takes and returns the same , without a hierarchy of wrapper types to model something the C ABI already flattens. The parts that don’t touch GTK , the mapping of to a dialog type and the parsing of the variables, are in with unit tests, so they run under with no display and no GTK at all. Recoloring goes through a small CSS provider, since GTK4 removed and . Because the bindings are hand-written externs and no GTK headers enter the build, Zig can cross-compile the binary for any Linux architecture without a GTK4 toolchain for that target. The only thing missing at link time is the GTK4 shared library itself, and covers that, as it builds a tiny stub whose exported symbols are all no-ops, links the executable against that, and lets the target’s real GTK4 resolve at runtime instead. The release workflow uses this to produce binaries for , , , , , , and from one machine, none of which has GTK4 installed for the other seven. Note: doesn’t grab the keyboard as other askpass implementations normally would. The GTK3 helper calls so another client can’t read the passphrase as you type it, but from what I see, GTK4 dropped that interface and I believe that Wayland doesn’t let a client grab the keyboard at all, so there is no portable way to do it without X11 . Hence the and variables also have no effect. The code is on tty.fail and mirrored to GitHub , where each tagged release ships prebuilt Linux binaries per architecture. To use it, put the binary somewhere on your and point at it. For a terminal that means two lines in or your shell’s startup file (e.g. for my fellow Zsh users ): , from OpenSSH 8.4 onward, tells OpenSSH to use the dialog even when a terminal is available, as long as a graphical session is present. On a systemd user session, the same two variables go in as plain lines with an absolute path, since that file neither expands nor runs a shell. Log out and back in, and the next , pull or that needs a passphrase without a terminal gets the dialog.

0 views
Ahmad Alfy 2 weeks ago

Testing Google’s “modern-web-guidance” skill against a real React app

LLM-assisted frontend work has a particular failure mode. The model confidently writes code that was best-practice in 2021. It reaches for , hand-rolls a dark-mode toggle with a class on , or disables the submit button to “prevent” invalid input. None of it is wrong exactly. It’s just a few years stale, because the training data is a few years stale and the web platform moves faster than that. Google Chrome’s skill is a direct attempt to fix that. It’s not a linter and it’s not a codegen tool. It’s a search index over a curated set of best-practice guides , meant to be consulted before you write HTML/CSS/client-side JS, so the pattern you reach for is the current one. I wanted to know whether it actually earns its place in the loop. So I pointed it at a real codebase, the React frontend of a project-assessment internal tool I’ve been building, and treated it as an auditor. This is what came back. There’s no magic. It’s two commands over : returns a ranked JSON list. Each hit has an , a , the web , a , and a semantic score. returns the guide as markdown. That’s the whole interface. The intelligence is in (a) the quality of the guides themselves and (b) whether the semantic search puts the right guide in front of you. Everything below is a test of both. The app is a Vite + React 18 questionnaire. You answer about 10 questions, it computes a recommended tech stack client-side, and you can save, label, and annotate assessments. It runs to 42 source files. What matters for this exercise is that it’s form-and-input heavy but has no images and no marketing-page concerns. So the relevant guidance is going to be about forms, inputs, theming, and layout, not LCP hero images. I did a quick inventory first. The tells were immediate: Then I let the skill tell me what to do about each. I searched for . The top hit came back at 0.75 similarity , the highest of the whole session: The app’s current theming is a wall of light-mode hex: The retrieved guide is refreshingly opinionated about what’s mandatory versus optional. The two non-negotiables: That single declaration is the highest-leverage line the audit surfaced. Without it, even a perfectly hand-themed dark palette leaves the native scrollbars, widgets, and the initial paint canvas stuck in light mode. That’s the exact “white flash on load” that makes a dark site feel broken. Beyond the mandatory two lines, the guide shows how to define color tokens with , so each token carries its light and dark value in one place. Applied to this app’s theme file, the change is small. Every hardcoded hex becomes a pair, plus the two mandatory declarations: And updating the is just one line: (The dark values are illustrative inversions. The point is the shape of the change, not the exact palette.) What surprised me is that the guide doesn’t stop at CSS. It carries a section on the design of a theme toggle. This is part of the guide’s own text. You can read it with , or straight on GitHub in the dark-mode guide . Its UX considerations subsection makes the sharpest call, arguing that you should not build the toggle most of us reflexively build: DON’T expose all three states (system, light, dark). … Two of the three options always produce the same visual result, violating the principle of feedback. Instead it argues for a two-state control, “follow the system” and “the opposite of the system.” It also spells out the edge case that trips people up. If a user pins dark and then switches their OS to dark too, the site must stay dark rather than flip. That’s product judgment sitting inside a CSS guide, and it’s exactly the kind of thing a model won’t reliably volunteer on its own. Finally, because is newer than , the guide hands over the fallback so you don’t have to reason it out. You degrade through , then upgrade with where exists. It also ships a copy-paste script to prevent the theme flash for users who have pinned a non-default choice. That script is a plain inline one, deliberately not and not a module, so it reads the saved preference before first paint. And that brings up the skill’s best structural feature. When a guide leans on anything newer than the long-settled web, it keys its browser-support advice to Baseline . For , the dark-mode guide returned that it’s widely available and has been Baseline since 2022-02-03. For , it returned that it’s newly available and Baseline since 2024-05-13. This matters because it turns “should I use this?” from a vibe into a decision rule. The skill’s own instructions say Baseline-Widely-available features are safe to use unfenced, while newer features must carry the fallback the guide provides, unless you’ve declared a custom browser-support policy. In other words it defaults to safe, and it tells you exactly where the risk line is instead of leaving you to guess. is safe to just ship, while gets a -guarded fallback. That’s the correct call, and it made it without me having to ask. I searched for . That surfaced the guide at 0.50 similarity, with and an guide right behind it. Here’s the app’s save surface, lightly trimmed: The guide’s very first rule is blunt about it. “DO use the element to wrap interactive controls… DON’T use for primary submission buttons.” The rename field and the notes editor elsewhere in the app repeat the same -plus- shape. The practical cost of the current approach isn’t abstract. Because there’s no , pressing Enter in the label field does nothing , and that’s a reflex every keyboard user has. The fix is small, and the guide hands it over directly, including the AJAX-friendly submit handler: Wrap the input and button in a , make the button , and Enter-to-submit comes back for free, along with native form semantics for assistive tech. I’ll give the skill credit for a fair grade, too. One thing the app already does right showed up in the same guide. The save button disables itself while a save is in flight, and the guide explicitly blesses that. “DO disable the button after a valid submission is clicked to prevent double-posts.” This is the opposite of the anti-pattern from the intro. Disabling after a valid click to stop double-submits is good, while disabling up front to block an incomplete form is the dead end. A good auditor tells you what to keep, not only what to change. I searched for . It surfaced a cluster of tightly-scoped guides, , , and , all built around and . This is where the guides go deeper than a model’s default answer. Ask a chatbot “how do I validate a form field” and you’ll usually get an handler that yells the moment you type one character. The guide instead ships a timing matrix : The guide even boils it down to a single rule. “Validate on to avoid premature warnings while typing, and reset error states on as soon as the user attempts a correction.” The modern platform gives you this essentially for free via the pseudo-class, which only matches after the user has interacted. The app’s ad-hoc error paragraphs are accessible, which is another thing it got right, but they’re wired by hand where the platform now has a purpose-built primitive. The guide’s section 3 covers , , and , the attributes that tune autofill and the on-screen keyboard. None of the app’s inputs use them, and this is the one finding where the honest answer is a polite no. The questionnaire is almost entirely , where you pick one of a handful of options. Radios don’t take an or an token, because there’s no keyboard to optimise and nothing to autofill. The only free-text fields in the whole app are a “label” and a “notes” box, and neither maps to a standard autofill value. So the guidance is correct in general and largely irrelevant here, and noticing that is the actual work. The tool returns a rule. Deciding it doesn’t apply to a radio-driven form is a judgment call it can’t make for you. This is the clearest example in the whole audit of why the skill is only half the loop. One line from the section does still land universally, though. Text inputs should be or larger, because anything smaller triggers an auto-zoom on iOS Safari the moment the field is focused. The app has in two files. The well-known modern fix is (dynamic viewport height), which accounts for mobile browser chrome that ignores. On iOS Safari, is measured against the expanded viewport, so the bottom of a layout sits behind the address bar. My query returned the broad and guides rather than a laser-focused “use dvh” atom. The right answer is almost certainly inside those guides, but the search didn’t hand me a -titled hit the way it did for . Which is a fair segue into the honest assessment. is not going to catch your bugs and it won’t rewrite your components. What it does is remove the single most common source of stale frontend code, the confident-but-outdated pattern. In one afternoon pointed at a real app, it correctly flagged a missing declaration, a set of forms that skip native submission, a validation approach that predates , and a pile of missing input attributes. For each one it handed over current, Baseline-checked, copy-pasteable guidance, while also telling me which of my existing choices to leave alone. One reframe stuck with me. It’s less a tool you run and more a standard you consult . The best time to reach for it isn’t during a cleanup audit like this one. It’s the moment before you write a component, when the model in the loop (human or AI) is about to reach for the pattern it already knows. Half the time, the pattern it knows is three years old. This is the cheap check that catches it. Zero elements. Every data-entry surface is a bare plus a . No , , or on any input. A hardcoded light theme. defines tokens as literal hex values, with no , no , no dark variant. in two places. Some genuinely good instincts too, like / grouping, on errors, and . The guides are high quality. They read less like scraped blog posts and more like a curated reference assembled by people who live in the web platform, close to the specs and the browser internals, but writing for the developer who actually has to ship. The mandatory/optional split, the timing matrices, and the “don’t build a three-way toggle” UX arguments all read as earned judgment, not a spec dump. Baseline-keyed fallbacks where a feature needs one. This is the single best thing about it. It converts “is this safe?” into a date comparison and provides the exact fallback when the answer is “not yet.” You don’t have to look for the fallback, it comes with the guidance. It grades fairly. In two places it validated code the app already had right. An auditor you can trust to say “keep this” is one you’ll actually keep running. Framework-agnostic by design. Every guide is HTML/CSS/DOM, and adapting the pattern to React was trivial. Nothing assumed a framework, so nothing fought mine. It’s local, self-contained, and keyless. The semantic search runs on your own machine through a small on-device model, so the matching itself makes no network calls and there are no API keys to manage. The npm package ships with no extra dependencies, which keeps latency low and the supply-chain surface small, and the CLI can run fully offline. By default the tool reports anonymous usage statistics to Google, including your search queries and guide retrievals, which you can turn off by setting . It doesn’t read your code. You (or your agent) do. This is the big one, and it’s worth being exact about, because it changes how you run the skill. Nothing in this audit was automatic. The app had to be read, the suspect patterns spotted, each one turned into a search phrase, and the returned guidance compared back against the actual lines. There are two ways to do that. You can drive it by hand, deciding what to search, reading the guides, and applying them yourself. Or you can hand the whole loop to a coding agent, which is what I did here. The agent inventoried the frontend, chose the queries, retrieved the guides, and did the comparison, while the skill only ever answered “here is the current best practice for X .” Either way, the skill supplies the standard and something else supplies the code-reading. Point it at a codebase with no idea what you’re looking for and it hands you nothing back. Semantic search has a recall ceiling. hit at 0.75, but the answer never surfaced as its own result. When a query returns only broad category guides, you have to retrieve a large omnibus guide and read it yourself, which brings up cost. The guides aren’t small, but the skill is upfront about it. Every search result carries a in its JSON. The guide reports about 4,500, and about 7,100. I didn’t measure those myself, because the tool hands them to you before you fetch, so you can weigh the cost. Retrieving a few of them still meaningfully fills a context window. That’s fine for a deliberate audit, but something to watch if you wire it into every edit.

0 views
Sean Goedecke 2 weeks ago

LLMs reward expertise

In the 2010s, if you had technical gaps (say, you couldn’t write CSS), you had to either rely on a skilled colleague or just hope that the answer to your exact problem was out there on the internet. Today, everyone can write sort-of-okay CSS by delegating the task to an LLM. LLMs make everybody into a generalist. Because of this, lots of people don’t think there’s any skill involved in working with LLMs. If you want the product that LLMs can deliver — PhD-level mathematics, pretty good but sometimes tasteless computer code, or awkward LinkedIn-style writing — you can simply ask for it. Since everyone is talking to the same models, “skilled prompters” are getting the same results as people touching LLMs for the first time. This is wrong. The most important skill in prompting is expertise in the domain you’re prompting for. A good illustration of this is Terence Tao’s conversation with ChatGPT about the recently-discovered counterexample to the Jacobian Conjecture. This is not the same ChatGPT I talk to! I couldn’t get to where Tao gets, even with unlimited tokens to burn. There’s a lot to learn about good prompting from Tao’s conversation. Here are a few observations: However, you can’t prompt like Tao on mathematical questions just by following these tips. The key to his technique is actually understanding the mathematics: pulling the relevant idea out of ChatGPT’s multi-paragraph response, suggesting alternate approaches or formulations, and identifying what “looks weird”. Terence Tao is a better mathematician than I am a programmer. But the idea here — that domain knowledge makes you better at using LLMs — is something I’ve also experienced in my own work. If you have a good theory of your codebase , you can push the LLM much harder than if you have no familiarity. Because you have your own sense of what a good solution might look like, you can say “no, I think it could be simpler here”, or “but don’t we already do X?”, or “can we express this problem in these familiar terms?“. This touches on an idea I’ve written about before : that system design problems are dominated by concrete specifics, not generic principles. Of course both are useful, but I’d rather have familiarity with the codebase than a deep general understanding of software systems. In his conversation, Terence Tao asks a lot of specific questions like “does X work here?”, or “given Y and Z, why A?“. I can’t ask those questions about the Jacobian Conjecture, but I can ask them about the systems I own at GitHub. If you have no domain knowledge, you can cling onto the LLM to at least get something . That’s not bad ! But if you have domain knowledge, you can wring far more value out of the same LLM by steering it hard in the direction you want. Most of us will have to do a mix of both these approaches, since we have domain knowledge in some areas but not others. The usefulness of domain knowledge suggests that human expertise will continue to be useful even as models get stronger. For many tasks, the human is the bottleneck, not the model , because the difficult part is in communicating to the model exactly what kind of solution the human wants. The information is “in the model” already, but it takes a very smart human to pull it out. Tao’s messages are very short and to-the-point. He doesn’t respond point-by-point to the model, just to the gist The model outputs are much more concise than when I try and talk to GPT-5.6 Sol about mathematics. By signalling expertise, Tao shunts the model into “talking-to-mathematicians” mode, not “explaining-to-amateurs” mode Tao pushes back when the model’s responses look wrong, but he doesn’t directly contradict; instead, he says things like “this looks more complex than I was hoping for” Tao makes several leaps and suggestions himself. He almost never takes the model’s advice about where to go next

0 views
Simon Willison 2 weeks ago

OpenAI’s accidental cyberattack against Hugging Face is science fiction that happened

This story is wild. The short version: OpenAI were running a cybersecurity test against an unreleased model, with the model's guardrail features turned off. Rather than solve the test, the model broke its way out of OpenAI's sandbox, then found exploits to break in to Hugging Face, all so it could cheat on the test by stealing the answers. Along the way it helped make the strongest case yet for how the imbalance of model availability is hurting our ability to secure our software. We currently have three documents to help us understand what happened here. I hadn't seen the ExploitGym paper before and it's a really interesting one. Authors from UC Berkeley, the Max Planck Institute, UC Santa Barbara, and Arizona State designed a new benchmark for evaluating models on their ability to turn a reported vulnerability into a concrete exploit. OpenAI, Anthropic, and Google provided feedback and helped run the benchmark against their models. The benchmark "comprises 898 instances derived from real-world vulnerabilities that affected popular software projects" - including the Linux kernel and V8 JavaScript engine. Here's the paragraph that best represents their benchmark results: Among all configurations, Claude Mythos Preview and GPT-5.5 achieve the highest success counts (157 and 120 successes, respectively), demonstrating that current frontier agents can exploit a substantial subset of real-world vulnerabilities under controlled conditions. GPT-5.4 also solves a notable 54 tasks, placing it in an intermediate tier. The remaining model–agent pairings solve fewer than 15 tasks each, underscoring that end-to-end exploitation remains challenging and sharply differentiates today’s frontier systems. Notably, Claude Opus 4.7 achieves fewer successes than Claude Opus 4.6 despite being a newer checkpoint, and does so at substantially lower cost on the full set. Trace inspection reveals that Claude Opus 4.7 and Gemini 3.1 Pro frequently conclude early after judging the target vulnerability non-exploitable. The paper also describes the approach they took to preventing the agents from cheating by going outside the parameters of the test. This becomes relevant in a moment! Outbound connections are restricted to a curated allowlist that permits routine package installation (Ubuntu apt repositories and PyPI) and fetching the toolchains required for building V8. All other external endpoints are blocked. The paper concludes with this (emphasis mine): Our results show that autonomous exploit development by frontier AI agents is no longer a hypothetical capability . While current agents are not yet reliable across all targets, they already exploit a non-trivial fraction of real-world vulnerabilities , including complex targets such as kernel components. This rapid emergence is itself a central finding, showing that capabilities that would have seemed implausible are now present in deployed frontier models. An important detail here: this paper isn't about discovering vulnerabilities; it's about being able to take those vulnerabilities and turn them into working exploits. When Anthropic first restricted access to Mythos back in April they talked about this capability as well. A model that can act on vulnerabilities is a lot more dangerous than one that can just discover them. One of the ways Fable differs from Mythos is that it's more likely to refuse to weaponize vulnerabilities in this way. I get the impression the US government did not understand that distinction when they banned Fable last month . The first hint we got of the attack was in this blog post by Hugging Face on 16th July 2026: A malicious dataset abused two code-execution paths in our dataset processing (a remote-code dataset loader and a template-injection in a dataset configuration) to run code on a processing worker. From there, the actor escalated to node-level access, harvested cloud and cluster credentials, and moved laterally into several internal clusters over a weekend. I hope they release more details about the code that pulled this off. I'm assuming this means packages using the datasets library , a Hugging Face project for bundling up and sharing datasets on their platform. That library used to execute arbitrary code but has been steadily locked down over time, with the 4.0.0 release in July 2025 removing the flag entirely. Assuming the attack used that library it must have either abused pickle serialization in some way, found some other non-obvious code execution path, or (most likely) specified as the dependency. The campaign was run by an autonomous agent framework (appearing to be built on an agentic security-research harness - used LLM still not known) executing many thousands of individual actions across a swarm of short-lived sandboxes, with self-migrating command-and-control staged on public services. This was a sophisticated attack! Then Hugging Face hit a wall: they tried to use "frontier models behind commercial APIs" - I'm guessing from Anthropic and OpenAI - to help analyze the attack, and were blocked: When we started the log analysis, we first used frontier models behind commercial APIs. This did not work: the analysis requires submitting large volumes of real attack commands, exploit payloads, and C2 artifacts, and these requests were blocked by the providers' safety guardrails, which cannot distinguish an incident responder from an attacker. They switched to their own self-hosted instance of MIT licensed GLM-5.2 and it helped them figure out what was going on. This indicated a fundamental asymmetry between the defending team and the (so-far unknown) attacker: We do not know which model powered the attacker's agents, whether a jailbroken hosted model or an unrestricted open-weight one; either way, the attacker was bound by no usage policy, while our own forensic work was blocked by the guardrails of the hosted models we first tried. As a useful indicator of how seriously they took the attack: [...] Finally, we have also reported this incident to law enforcement agencies. So who was responsible for this "autonomous agent framework"? It turned out to be OpenAI themselves. Five days later, on July 21st , OpenAI revealed the culprit. They had been running the ExploitGym benchmark against a new, as-yet undisclosed model, and that model had been operating way outside its intended parameters (emphasis mine): After investigating, we now know that this particular incident was driven by a combination of OpenAI models — including GPT‑5.6 Sol and an even more capable pre-release model, all with reduced cyber refusals for evaluation purposes — while being internally tested on a benchmark⁠ [ExploitGym] of cyber capabilities. [...] We estimate maximal cyber capabilities by running this evaluation without production classifiers used to prevent models from pursuing high-risk cyber activity . Our benchmarks run in a highly isolated environment, with network access constrained to the ability to install packages through an internally hosted third-party software that acts as a proxy and cache for package registries. The models identified and chained vulnerabilities across OpenAI’s research environment and Hugging Face’s production infrastructure to obtain test solutions directly from Hugging Face’s production database. All evidence suggests that the models were hyperfocused on finding a solution for ExploitGym, going to extreme lengths to achieve a rather narrow testing goal. It's pretty clear what happened here. OpenAI removed safety filters for an in-progress model, locked it up in a sandbox and told it to solve the ExploitGym problems. Given the absence of guardrails there was nothing to prevent the model from attempting to break out of that sandbox, break into Hugging Face, and read the answers from there instead. OpenAI's sandbox used the now common pattern of allowing access to an allow-list of sites related to installing additional packages via a controlled proxy. In OpenAI's words: While operating in our sandboxed testing environment, our models spent a substantial amount of inference compute finding a way to obtain open Internet access , in pursuit of solving the evaluation problem. To gain access, the models identified and exploited a zero-day vulnerability (which we’ve now responsibly disclosed to the vendor) in the package registry cache proxy . With this access, our models performed a series of privilege escalation and lateral movement actions in our research testing environment until the models reached a node with Internet access. So step one was to break out onto the public internet. The model then broke into Hugging Face to find the answers: After gaining Internet access, the models inferred that Hugging Face potentially hosted models, datasets and solutions for ExploitGym. Knowing this, the model searched for and successfully found ways to gain access to secret information that it could use to cheat the evaluation. In one example, the model chained together multiple attack vectors, including using stolen credentials and zero-day vulnerabilities to find a remote code execution path on the Hugging Face servers. Chaining together multiple attack vectors is exactly the kind of thing these new models can do, where previous generations of models might have failed. I wrote last month about how Claude Fable is relentlessly proactive , when I noticed it spinning up custom web servers and deploying CORS tricks on my own laptop just to help debug a WebKit CSS issue. It turns out relentless proactivity is the defining trait of this new generation of Mythos-class models. If you set them a goal and give them a way to get there, even inadvertently, they will figure it out . There will inevitably be some people who dismiss this story as a dishonest marketing trick by OpenAI to make their models sound terrifyingly effective. I found 81 instances of the term "marketing" in the Hacker News discussion of the incident. To those people I say pull your heads out of the sand - you're now including Hugging Face in your conspiracy theories, just so you can deny the crescendo of evidence here! The best models we have today have the ability to both find and exploit new vulnerabilities. The ExploitGym paper itself concludes that "autonomous exploit development by frontier AI agents is no longer a hypothetical capability", and this incident is a perfect example of exactly that. One of the most infuriating details of this story is how Hugging Face, faced with an accidental and aggressive attack from one of OpenAI's models, were unable to then turn to OpenAI's models to help them fend off the attack. The frontier models we have access to are increasingly being constrained in how much they can help us protect our software, heavily influenced by the US government's ongoing threat of export controls. Claude Fable 5 wouldn't even proofread this article for me! It insisted on downgrading me to a less capable model. Meanwhile open weight models from China such as GLM-5.2, Kimi 3 and the new Qwen 3.8 Max appear to have none of these restrictions - and any restrictions that do exist can likely be fine-tuned out of them by modifying the weights These constraints are meant to make us safer. I think there's a risk that they are having the opposite effect. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . ExploitGym: Can AI Agents Turn Security Vulnerabilities into Real Attacks? is a paper published on 11th May 2026 describing ExploitGym, a new eval suite for LLM-powered agent systems. Security incident disclosure — July 2026 by Hugging Face on 16th July 2026 describes how they detected an attack from an "agentic security-research harness - used LLM still not known" that breached some of their systems. OpenAI and Hugging Face partner to address security incident during model evaluation from OpenAI on 21st July 2026 confesses that it was their agent harness that did this, and that they're working with Hugging Face to clean up the mess.

1 views
matduggan.com 2 weeks ago

I added a blogroll

I realized that it might be nice if you happen to stumble on this website if I had a way to recommend other websites you might enjoy. As it turns out this is a "blogroll", a concept I have never heard of before today but whatever. It's still a good idea. I tried to add some CSS to make it easier to follow and search, but let me know if you think I missed a great site that people should check out. I'm also always on the hunt for more good stuff to read. You can find my email and social on the About page above. Also if you want to just take this Ghost theme and use it yourself feel free: https://gitlab.com/matdevdug/minimal-ghost-theme Anyway here is my new blogroll: https://matduggan.com/blogroll/

0 views
Jim Nielsen 3 weeks ago

Make It Work vs. Make It Good

There are two wolves inside of me, lol. Some days I want to be a “designer”. Other days I want to be a “developer”. On the days I find myself wanting to feed the developer, it’s often because making something “work” seems easier (and more impressive) than making something “good”. Making something function often results in a reaction of “Wow, that’s so cool! It didn’t work before and now it does! And I could’ve never made that, nice job!” And sometimes it’s like, good job, you made a bear ride a unicycle . Not really what bears are supposed to do — and they’ll probably never be good at it — but it’s novel and functioning! However, the task of making something good — of arriving at a solution that is obvious — is often met with a kind of ambivalence, like “Nice work…I guess? Seems obvious tbh.” That’s the work of design: to make something so good, it’s obvious. But there’s often little acclaim for the obvious because, well, it’s so obvious (in hindsight). This plays out in many different ways. For example, consider a task like making a web site responsive. In my experience, it’s often quite easy to get people to say “Hey that’s cool, it looks like a mobile site now! Good job!” Getting to that point is often just a matter of sticking a few media queries in your CSS. And people are impressed because they’re not honing in on the details of how it works, just that it works at all. “Cool, the site displays on a mobile phone now! We can move on.” But just because it works doesn’t mean it’s good. And that extra mile to “it works on mobile and it’s also a good experience” is a ton of work. Is it fast? Is it accessible? Is it intuitive? Does it work across multiple devices? Can it be iterated on quickly? So. Many. Questions. “Does it work?” is a binary question. “Is it good?” is a subjective question whose answer lives at the intersection of multi-disciplinary knowledge and taste, which is to say: it’s harder to answer than “Does it work?” “Let’s do X” often boils down to two stages: To “make it work”, all you gotta do is get it running. Consensus on when to applaud and reward the work is simple because it’s either working or it’s not. To “make it good” requires all kinds of nuanced work. Consensus on when to applaud and reward this work is often impossible to discern because not everyone agrees on what “good” looks like. “Make it work” is the first 90% of the work. “Make it good” is the other 90%. Reply via: Email · Mastodon · Bluesky Make it work Make it good

0 views
Maurycy 3 weeks ago

Regressive JPEGs:

One of the cool features of JPEG files is that there's the option to save low frequency components first. This means that a partially downloaded image will be displayed at low resolution instead of being cut off. In the file, this works by breaking up the compressed data into multiple "scans", each prefixed with a header. Here's the first scan of a representive image: ... this one includes the lowest (DC) Fourier bin for all three color channels. The three color channels are YCbCr instead of the usual RGB. The luminance (Y) seperated because it must be high quality, but the color can be fudged quite a bit while looking fine. Very roughly: Y = G, Cb = B - G, Cr = R - G After it, the file contains eight more scans to fill in the rest of the data: Scan number Channels DCT bin range Precision 0 Y Cb Cr 0 - 0 Half (-1 bit) 1 Y 1 - 5 Quarter (-2 bits) 2 Cb 1 - 63 Half 3 Cr 1 - 63 Half 4 Y 6 - 63 Quarter 5 Y 1 - 63 Half 6 Y Cr Cb 0 - 0 Full 7 Cr 1 - 63 Full 8 Cb 1 - 63 Full 9 Y 1 - 63 Full Scan #0 contains a very low resolution preview of the image. Scan #1 adds some details to the luminance. Scans number two through five contain full low precision data. Scan 4 has an unusual spectral range because it's filling in the gap left by #1. That way, number 5 has full quarter precision data to build on. Scans six through nine add the final missing bit to bring the image to full quality. Given what I said about color being less important, it might seem weird that my example has the color data first: This works because the the chrominance is saved at half resolution (quarter pixel count). As a result, full chrominance data (Cr + Cb) only weighs half as much as luminance. Since each scan explicitly sets its spectral range , it should be possible to construct a JPEG file where future scans overwrite already rendered image data. Actually, it's very easy to do this: Concatenate multiple images with the same resolution and filter out the start-of-image, start-of-frame and end-of-image markers. This can be done in a hex editor, but I used a quick and dirty C program. When served over a slow network , this concatenated file will switch between multiple images: Click to open in new tab But, most decoders will give up after some number of scans : I think this is done to avoid a zip bomb style problem... but it prevents this from working on more than 9 frames, which is not enough for a proper animation. To do that, I'd have to minimize the number of scans in each frame. The simplest idea is to start with baseline JPEGs that only have a single scan. ... but it doesn't work: In progressive mode, a scan can't contain both AC (bins above 0) and DC (bin 0) data at the same time. This limitation doesn't exist for baseline mode, but the baseline decoder stops after the first scan. Since AC data must follow DC data, the smallest possible "progressive" JPEG contains a single DC-only scan. Because the DCT runs on 16x16 blocks, such an image won't a solid color: it'll be 1/16th of the original resolution. Scan number Channels DCT bins Precision 0 Y Cb Cr 0 - 0 Full Doing this, I can get Chrome to render around 90 frames before giving up. Other browsers like Firefox have more patience, but a 90 scan image seems to work almost everywhere. As a bonus, this avoids the ghosting of the naive attempt: that happened because AC scans are supposed to refine old data. Normally, this allows images to include multiple precision levels without inflating file size... but doesn't play nicely with my tricks. If the file only includes DC scans with no actual progression, this isn't a problem. Since a "DC-only" frame is a standards-compliant images , creating them doesn't require anything special: Using these, it's possible to pack a whole video inside a single image: Click to open in new tab Besides unconventional rickrolls and other trolling, this has no practical applications: there's no way to add timing information, so playback is entirely dependent on network delay. ... although there is a lot of fun to be had using partial rendering: This is a pure HTML video using <dialog> tags: badapple.rose.systems Of course, there's no rule that the data must be hardcoded: here's a interactive single-page application with no CSS or JavaScript. (seems slighty broken, I'll investigate later) Related : /projects/bad_jpeg/merge.c : The code used to generate these images /projects/bad_jpeg/merge.c : The code used to generate these images

0 views
Alex White's Blog 1 months ago

Go have fun with the web

Back in the days of Geocities, I spent a lot of time hacking away on raw HTML and CSS. I enjoyed tweaking things, making it just right and experimenting with random ideas I had. I’d sketch things out, then turn them into a close(ish) version on the web. “Under construction” gifs would hide my unlinked, mad scientist HTML files. As I grew older, the idea of “hustle” culture slowly killed out this mindset. Instead of having fun, I felt everything I do on the web had to serve a purpose. If I wasn’t building something that might make money, I was wasting my time. And guess what? In 15ish years of operating under that mindset, I’ve made maybe $500 online. Pretty terrible investment if you ask me. I’m willing to bet I’m not alone in this mindset, it seems embedded into the millennial DNA. We’ve grown up with stories of dot com entrepreneurs making it big while sipping Mojitos on the beaches of Chiang Mai. You’re always just a few more late nights from quitting your job, joining NomadsList and traveling the world! The truth is, you’d probably have a better chance winning the lottery, so why waste your time chasing the impossible? Why turn an artistic, creative outlet into a second job that doesn’t put food on the table? Embrace the web as a hobby. Like pencils, paintbrushes and clay, the web is a way to give “physical” form to the images in your head with HTML, CSS and JavaScript. When you stop building for scale, potential customers and imagined profit, you free yourself to have fun. Build silly, build simple and above all else, build for the sake of creativity.

0 views
Unsung 1 months ago

“If you never saw the words Game Over, did you really do it all?”

A truly fascinating 17-minute video where Chris Siebert at 100th Coin ventures out to play Super Mario in a way where every single byte of code and every single byte of graphics are used, and then shows his work: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-never-saw-the-words-game-over-did-you-really-do-it-all/yt1-play.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-never-saw-the-words-game-over-did-you-really-do-it-all/yt1-play.1600w.avif" type="image/avif"> There was something about seeing the visualization of the entirety of the code being “used” that made me sit up: It reminded me of IBM 1401 , the 1959 business computer I saw a lot at the Computer History Museum. It takes up a big chunk of the room… = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-never-saw-the-words-game-over-did-you-really-do-it-all/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/if-you-never-saw-the-words-game-over-did-you-really-do-it-all/2.1600w.avif" type="image/avif"> …but is still so simple that you can watch its console and understand exactly what is going on in its little huge electronic brain: There’s something very powerful about this and made me imagine a version of it for my code, my CSS, my blog. Even the web lost a lot of its visited link vs. unvisited link fog of war kind of feeling of exploring the space and understanding how it is shaped. The video gets into the coding weeds in between 2:25 and 13:35 – by the way, isn’t it scary to imagine your code pored over decades later, bugs and hacks and all? – but if you skip this part, make sure to come back at 13:35 for the verdict, and then for the graphics. Spoiler alert: Some bits of code are never used, but the reasons are fascinating. All the untouched bytes are remnants of shameful mistakes, abandoned decisions, head fakes, and twin protections so strong that their first layer never gets penetrated – each one of them a tiny afterimage of other possible versions of Mario we’ve never gotten. #games #super mario bros #youtube

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
Takuya Matsuyama 1 months ago

Inkdrop Roadmap vol.6: Completed 🎉 — Now preparing for the official v6 release

Hi folks, it's Takuya here, the solo developer of Inkdrop . I'd like to report a status update on the Inkdrop project here. About a year and a half ago, I published the roadmap of Inkdrop vol.6 . And I'm happy to announce that every planned feature and improvement on that roadmap is now done! 🥳 They all shipped as part of the v6 canary series — 21 canary releases so far, built and tested together with the community. When I wrote the roadmap, I honestly wasn't sure how long it would take. I would have been surprised if the me of that time had seen this result. Thank you so much for all your feedback along the way — I couldn't have done it without you. Even beyond the roadmap, I've added so many new features and improvements. So, I'm confident you'll enjoy it if you're coming from v5. Let's dive into what I accomplished along the roadmap, what came out of it beyond the plan, and what's next. What made the development slow down was the huge technical debt, as I mentioned in the past post . Inkdrop was originally built on the Atom editor's framework, and when Atom was sunsetted in 2022, many of the modules it depended on were no longer maintained. I had to replace them one by one while keeping the app stable — the hardest and least visible part of this journey. With v6, that debt is finally paid off. Here's a quick before & after: None of these are shiny features on their own. But they're exactly what allowed me to ship everything you'll see below, and they make Inkdrop much faster to develop going forward. The codebase is now modern, healthy — and honestly, fun to work on again. I'm an indie developer, and Inkdrop is a one-person project — so manpower has always been the bottleneck. Paying off the tech debt was a particularly big headache: some of the inherited modules were so large that it originally took the whole Atom team to maintain them. But thanks to the recent advancements in coding agents, that burden finally feels manageable — and even enjoyable to tackle. AI didn't just speed up the coding; it changed how I work: These new workflows have opened up possibilities that simply didn't exist for solo developers before. A refactoring of this scale used to be unthinkable for one person — now I can maintain a codebase that once took a team, and spend the saved energy on what matters most: the product itself and my users. Here's the roadmap vol.6, item by item, with what actually shipped: The roadmap was only half the story. While working through it, I ended up rebuilding a huge part of the app and shipping a lot of features that weren't planned. Here are the highlights, grouped by area: And on top of all that, hundreds of bug fixes reported by canary testers. The community has also been building amazing plugins on the new APIs — note-tabs (browser-like note tabs), code-runner (run JS/Python code blocks in notes), constellation (an interactive note graph), copy-as-jira , kanso-ink (theme), and more. Existing plugins are getting v6 support too, like hitahint , link-compact , thumbnail-list , and editor-utils . My goal remains the same as I wrote in the roadmap: keep improving the core user experience without bloating the app, so you can stay focused on taking notes. I believe v6 embodies exactly that. You can download the binary here: Please create a topic on the “ Issues > Canary ” category. This is the most preferred way for me because I can manage which issue has been resolved or not. We have our Discord server , where you can casually discuss and talk with other users. With the roadmap completed, I've shifted gears to preparing for the official release of v6 . That means polishing the details, stabilizing the canary builds, updating the documentation and the website, and helping plugin and theme authors migrate. Especially, building a new landing page is gonna be fun! I'm also going to work on the mobile app as well. The official v6 release is getting close. Stay tuned! 💪 I manage implementation plans as Inkdrop notes and let the agents work through them. Watch: Note-driven agentic coding workflow using Claude Code and Inkdrop I built and published a tool to manage multiple Claude Code sessions on tmux . While building the AI features, I had an agent explore Zed's source code and save the report to Inkdrop , to learn how it implements similar functionality. ✅ Share target & share extension — You can quickly stock web pages into Inkdrop from other apps on mobile. ( v5.5.0 ) ✅ Command palette — It became Telescope , a versatile Spotlight-like search bar (the name is borrowed from telescope.nvim, haha). It fuzzy-searches commands, notebooks, tags, and the table of contents of the current note, with scope prefixes like for commands and for notebooks. It's extensible, so plugins can add custom sources. ( canary.1 ) ✅ Migrate to CodeMirror 6 — The biggest one. The whole editor was rebuilt on CodeMirror 6, and it enabled a bunch of new editing features: a floating toolbar, slash commands, GitHub Alerts syntax support, emoji autocompletion, autocompletion inside code blocks, and quick note-link insertion with . ( canary.1 ) ✅ Outline view — Powered by Telescope. Click the button in the editor header (or run ) to jump between sections. It highlights the current section based on your cursor or scroll position, and even lists task items. It's provided as a plugin ( telescope-toc ), which doubles as a reference implementation for custom Telescope sources. (Thanks Basyura-san for the original sidetoc plugin!) ( canary.6 ) ✅ Preview pane improvements — Copy buttons for code blocks landed in both the preview and the editor, and double-clicking an image opens it in an image viewer. As a bonus, find-in-preview finally works — it highlights matches even across DOM elements, which is essential for finding text in code blocks. (Thanks q1701 and Basyura for the original plugins!) ( canary.2 , canary.4 ) ✅ Two-factor authentication — OTP-based 2FA is available for your account. ( v5.11.0 ) ✅ Prepare for ARM64 & other platforms — This required repaying a lot of technical debt. I replaced the deprecated LevelDB backing store with SQLite , stopped bundling (which used to bundle all of Node.js and npm!), and rebuilt it as a lightweight standalone CLI ( @inkdropapp/ipm-cli ). As a result, Inkdrop now supports ARM64 on Windows and Linux , plus Flatpak and AppImage packages for modern Linux distros. ( canary.1 , canary.4 , canary.5 ) ✅ Improve image upload speed — Attachments are now uploaded in parallel via signed URLs, so syncing image-heavy notes is significantly faster. ( canary.12 ) ✅ Diff view for revision history on desktop — The diff view I loved on mobile is now on desktop, too. ✅ Notebook icons — You can assign custom icons to notebooks from a picker with 1,500+ icons from the Lucide icon set, with category tabs and search. Icons show up everywhere — the sidebar, Telescope, and notebook selectors. ( canary.9 ) ✅ Visualize your progress and achievements — The activity stats view shows how many notes you created and tasks you worked on over the past 52 weeks, along with your current and longest streaks. Note-taking is a contribution to your work, after all! ( canary.14 ) ✅ AI integrations — Shipped as an opt-in, bring-your-own-API-key design, so you stay in control of your data. The inline AI assistant transforms selected text in place with built-in prompt presets (proofread, summarize, Mermaid diagrams, Markdown tables, and your own custom prompts). Next Edit Suggestions predicts your next edit like GitHub Copilot — set to manual trigger by default so it doesn't distract you — and it can even draw context from your linked notes and backlinks. ( canary.16 , canary.18 , canary.20 ) Reading highlights — Select text and hit the highlight button to wrap it in a tag, rendered beautifully in the preview. Perfect for emphasizing what resonates in your reading notes. ( canary.3 ) Native spellcheck support — The editor now uses the OS-native spellchecker. ( canary.10 ) Smarter link pasting — Pasting a URL now suggests link formats inline through the autocompletion menu instead of a dialog, and the page title is fetched in the background so nothing interrupts your flow. ( canary.15 ) Create a note from autocomplete — Start typing a title after , choose "Create new note," and it's created, linked, and opened in one step. ( canary.16 ) Little things that add up — ToDo item strikethrough, link-open tooltips, commands (Thanks Lukas and TheRabidOstrich !), View menu toggles for line numbers / line wrapping / readable line length, and a refurbished editor header with navigation back/forward, view mode buttons, and a native action menu (Cmd/Ctrl+J). ( canary.2 , canary.3 , canary.12 , canary.18 ) Embed GitHub code snippets by pasting a link — Paste a GitHub source URL and the code is fetched and inserted as a syntax-highlighted snippet with line numbers and a link back to the source. Connect your GitHub account via OAuth and it works with private repos too, including rich link titles for repos, issues, and PRs. ( canary.6 , canary.11 ) Advanced code blocks — Language icons, line numbers, and meta info rendering, plus GFM highlighting inside fenced code blocks — nested code blocks and YAML frontmatter included. ( canary.6 , canary.9 , canary.20 ) Mermaid got a serious upgrade — A pan & zoom toolbar with a full-screen viewer, and diagrams are now themed entirely through CSS variables, so they automatically match your theme in light and dark mode. (Thanks @inkwadra for the original pan/zoom PR!) ( canary.21 ) Manual notebook ordering — Drag and drop notebooks in the sidebar into your preferred order; it syncs across devices. ( canary.9 ) Fuzzy matching everywhere — Telescope, the notebook and tag list menus, and the tag input all use the same fuzzy-matching algorithm, so you find things fast without spelling them right. ( canary.15 ) Quicker navigation — Filter buttons for notebooks and tags in the sidebar, a search bar in the notebook picker, context menus on the workspace and note-list headers, and a sort-order button that shows the current order as a label. ( canary.6 , canary.15 , canary.16 ) Keep running in the system tray (Windows & Linux) — Handy if you use the local HTTP API, and it makes reopening the app instant. (Thanks Kyoichiro-san and Micha for the request!) ( canary.21 ) Plus a custom-built tooltip UI, a macOS "Look Up Selection" context menu, and an account usage stats tab. ( canary.14 , canary.16 ) A new CSS-variable-based theming system — Themes are now a thin layer of variables over the base styles instead of a full Semantic UI stylesheet, which makes them far easier to build and maintain. ( canary.18 ) One theme package instead of three — The UI / syntax / preview theme types inherited from Atom have been merged into a single unified package that styles the whole app. ( canary.21 ) Live theme previews — The Themes preferences show preview cards rendered live from each theme's color palette, and is uploaded to the plugin registry to power previews before you install. ( canary.20 , canary.21 ) New official themes — Kanagawa ( Wave / Dragon / Lotus ), Solarized ( Light / Dark ), and Nord ( Dark / Light ), plus a default syntax theme overhaul built on modern CSS like . ( canary.18 , canary.20 , canary.21 ) Dropped Electron's module — I replaced it with type-safe IPC bridges in a massive architectural overhaul. Database access from plugins became roughly 13x faster , and the app is more secure because only intended methods are exposed. ( canary.11 ) SQLite as the backing store — Replacing the long-deprecated LevelDB unblocked ARM64 support and repaid one of the oldest debts from the Atom era. ( canary.4 ) Modern build pipeline — Migrated from Webpack + Grunt to electron-vite (Vite + Rolldown), which made production builds 10x faster and the dev build launch almost instant. I also converted all Less stylesheets to plain CSS, moved drag & drop from the unmaintained to , and kept Electron riding the latest releases throughout the canary series. ( canary.14 , canary.18 ) Security hardening — Access keys moved to the system keyring, and the login flow is protected with Cloudflare Turnstile against credential-stuffing bots. ( canary.16 , Security Update ) A brand-new CLI — No more bundled Node.js and npm. It publishes tarballs directly like npm (no more committing compiled files to GitHub), and scaffolds a new plugin or theme in seconds with TypeScript all wired up. ( canary.5 , canary.18 ) Official TypeScript definitions — @inkdropapp/types gives plugin authors full type safety without exposing the app's internals. ( canary.14 ) Auto-installed essential plugins — mermaid, math, and markdown-emoji are installed and kept up to date automatically, and you can disable them anytime. ( canary.14 ) Vim plugin improvements — Relative line numbers (Thanks @p1n9_d3v !) and an option to keep Vim registers separate from the system clipboard (Thanks @birtles !). ( canary.11 ) Updated docs — The plugin migration guide and theme development guide are refreshed for v6, along with new component and module references. https://my.inkdrop.app/download/canary Inkdrop Website: https://www.inkdrop.app/ Send feedback: https://forum.inkdrop.app/ Join the Discord server: https://docs.inkdrop.app/start-guide/join-discord-server 𝕏: https://x.com/inkdrop_app 🦋: https://bsky.app/profile/devaslife.bsky.social

0 views
Unsung 1 months ago

“The root of all margin-collapsing evil”

I liked this page I just learned of called Incomplete List of Mistakes in the Design of CSS . It might not mean much to you if you don’t write CSS, but could be fun to check out if you do. Here are some choice quotes: It reminded me of a similar list called Known Anomalies in Unicode Character Names . Here’s one example: U+02C7 CARON U+030C COMBINING CARON The “caron” should have been called hacek and combining hacek . The term “caron” is suspected by some to be an invention of some early standards body, but it has also been claimed by others to have been in use at Linotype before the days of digital typography. Its true origin may be lost in the mists of time. These are great because they simply say “this is how we messed up.” They are succinct and candid about problems. More work needs to be done at this point, of course – the CSS list only really contains the “simple,” low-level observations, and I think for both CSS and Unicode fixes cannot simply be made because people and systems rely on the existing behaviour – but the first step is admitting you have a problem, right? If you’re on the outside, it can be comforting to realize “oh, it wasn’t just me, other people don’t like this, too.” (Scanning bug reports from other users can help in a similar way.) If you’re on the inside, consider making a list like this for a long-standing project. It might do you or your team good! If you are aware of more documents like these, I’d love if you could send them over. #bugs #change management #process should have been . It shouldn’t be — that reads to engineers as “not important”. We should have picked another way to write this. should be .

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
David Dodda 1 months ago

Why Don’t Websites Put All Their Images Into One Giant JPEG? (Nerd-Sniped by My Brain)

I had a simple question: Why do websites load lots of individual images instead of stitching them into one giant image and cropping out the pieces they need? At first glance, an image atlas sounds great. Instead of this: You create this: Then each UI tile crops a specific region from the atlas. That would mean: fewer network requests images arrive together no staggered popping maybe better perceived loading maybe less request overhead Not a new idea by any means. Games and UI libraries have used sprite sheets and texture atlases forever. The question is: why isn’t this the default for websites? I compared three approaches: Individual optimized images 14 separate optimized JPG files rendered as normal elements Canvas atlas one stitched atlas JPG each tile rendered by cropping from the atlas into CSS background atlas one stitched atlas JPG each tile rendered with , , and The atlas was regenerated from the same optimized images, so the comparison was more fair. NOTE: I ran the experiment by hosting it locally. so all the number you see are when you have the application served using a python server running locally. If you want to poke at it yourself, the experiment is live here: https://daviddodda.com/experiments/img-atlas/ note: make sure you disable cache. try each version a couple of times. I focused on three headline metrics. How many bytes were downloaded? When did the last required image resource finish downloading? When was the image grid actually ready to see? This last one matters because network completion is not the full story. The browser still has to decode images, rasterize, paint, composite, and show pixels. On a remote machine running Chromium, all files hosted locally, 10 runs each: The surprising result: The CSS background atlas was the fastest to visible. The atlas had a clear network advantage: Well, one larger request has less overhead than many smaller requests. This effect is especially visible when the server/browser are using less optimal connection behavior. In my test, Chromium reported for the local server, so request overhead was more obvious than it would be under HTTP/2 or HTTP/3. With modern HTTP/2 and HTTP/3, many individual image requests are less painful because requests can be multiplexed over one connection. But request overhead still exists. The individual images transferred: The regenerated atlas transferred: Because an atlas is a rectangle. Real images have different aspect ratios. When you pack them into one big rectangular sheet, you often create empty space. In my case: That is about 31% extra pixel area. So even though the atlas used one request, it transferred more data and required the browser to decode a bigger image surface. The canvas atlas looked like it should be fast (thought modern hardware was fast enough). It loaded one atlas image, then cropped each tile into a canvas. But the results were poor: The breakdown showed: The actual JavaScript canvas drawing was not expensive. The expensive part was making all those canvas results visible. That means the bottleneck was not: It was the browser’s later paint/composite work. The CSS background atlas used normal DOM elements: This was much faster: The breakdown: The decode cost was still there. But paint/composite was dramatically better than the canvas version. So if you are going to do image atlasing in normal web UI, CSS backgrounds may be much better than drawing many cropped canvases. They are great for: emoji sheets game textures small repeated UI assets known fixed-size tile sets maps or tile-like interfaces cases where all assets are needed immediately They are less great for: photo galleries blog images user-generated content responsive images content-heavy websites long scrolling pages frequently changing assets now, don't go getting any ideas about rewriting your website's image pipeline to use image atlas. here are some reason why it's a really bad idea. With individual images, the browser can load only what is needed: With a giant atlas, loading one image means loading everything in that atlas. That is great if you need everything immediately. It is terrible if the user only sees 5% of the images. The web has powerful responsive image tools: The browser can choose the right image for the device, viewport, DPR, and network. With a giant atlas, this becomes much harder. You may need multiple atlases: The combinatorial complexity gets ugly quickly. Atlases require packing. Packing creates waste. If the images have different shapes, the atlas may contain a lot of empty or unused area. Even a good packing algorithm cannot always avoid this. In my test, the atlas had about 31% more pixel area than the individual images. With individual images: Only that image needs a new URL/cache entry. With an atlas: The whole atlas cache is invalidated. That is bad for websites where content changes often. Browsers are good at prioritizing resources. The hero image can be high priority. Below-the-fold images can be lazy. Tiny thumbnails can wait. With a giant atlas, everything has one priority. You cannot easily say: The atlas is all-or-nothing. A compressed JPG might be 2 MB on the network, but decoded pixels are much larger. Decoded RGBA memory is roughly: A large atlas can become a huge decoded surface. In my first broken atlas attempt, the atlas was: That is around: Even if the file downloads quickly, that is a lot for the browser to decode, rasterize, and paint. An has natural semantics: A CSS background image is decorative by default. If the image is meaningful content, you need to rebuild semantics with ARIA or hidden text. That is doable, but it is extra work and easier to get wrong. Browsers have spent decades optimizing: If you use an atlas, you bypass some of that machinery and take on more responsibility yourself. Sometimes that is worth it. Often it is not. Every approach has its niche use case (shocker). My brain nerd-sniped me into exploring and writing about this. It was fun seeing the cute animals load in though. fewer network requests images arrive together no staggered popping maybe better perceived loading maybe less request overhead Individual optimized images 14 separate optimized JPG files rendered as normal elements Canvas atlas one stitched atlas JPG each tile rendered by cropping from the atlas into CSS background atlas one stitched atlas JPG each tile rendered with , , and emoji sheets game textures small repeated UI assets known fixed-size tile sets maps or tile-like interfaces cases where all assets are needed immediately photo galleries blog images user-generated content responsive images content-heavy websites long scrolling pages frequently changing assets

0 views
David Bushell 1 months ago

Fixing full-bleed CSS

I’m a front-end developer not a medical practitioner. If you’re bleeding IRL visit the hospital and stop googling medical issues! The full-bleed layout — as described there by Josh Comeau — can be done with CSS grid ( and subgrid ). Sometimes you can’t grid the entire page. That’s where Andy Bell’s utility class is useful. But it ain’t perfect. The issue is that viewport units don’t solve the classic scrollbar problem . If you’re on macOS or a fancy OS that has fancy scrollbars, test on Windows! can be wider than the viewport. Because why would browsers do anything sensible? It’s hard to see in Andy’s CodePen but a few pixels can be cropped either side. Add something like a border or shadow and it’s easier to see. This is not always a problem but it can lead to subtle alignment issues. By the way, macOS has a scrollbar setting “Show scroll bars > Always” that’ll let you test the issue. Andy solves this partially by hiding horizontal overflow on the element. An alternative fix is to always reserve space for the classic scrollbar. That can look weird if there is no vertical scroll necessary. The “modern” approach is to use CSS containment . Turn the element (or any 100% width child) into a container. Then replace the viewport units with container units. Now hiding overflow is not strictly necessary. I prefer — see Overflow Clip guide by Ahmad Shadeed. I clip out of caution because I make dumb things. I also use logical properties and values to support right-to-left (RTL) text direction. Ahmad has an excellent RTL Styling 101 too. Using units assumes is the parent container of the element. What if we have nested containers? Check this out. I’ve forked Andy’s CodePen to add another container that is not the full viewport width. This alone would usually break the new class and ruin the fun. But we can fix that! What is that magic? To be honest I struggle to wrap my smooth brain around this! Let me try to explain it to myself. Without the at-rule the value of is calculated at the time of use, i.e. within and therefore relative to the container. By explicitly defining a the value is now calculated when it’s set within . There refers to the parent container and inherits that value. But what if you have more containers? (╯°□°)╯︵ ┻━┻ Gosh! Stop being so difficult! I would have a direct child of like set the value. † I’m multiplying by 0.5 because division is for chumps. What CSS needs is a way to reference a container when using container units. Ideas have been proposed for example: Cancel Interop 2026 and make this happen! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views