<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: css</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/css.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-07T21:25:14+00:00</updated><author><name>Simon Willison</name></author><entry><title>GLM-5.1: Towards Long-Horizon Tasks</title><link href="https://simonwillison.net/2026/Apr/7/glm-51/#atom-tag" rel="alternate"/><published>2026-04-07T21:25:14+00:00</published><updated>2026-04-07T21:25:14+00:00</updated><id>https://simonwillison.net/2026/Apr/7/glm-51/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://z.ai/blog/glm-5.1"&gt;GLM-5.1: Towards Long-Horizon Tasks&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Chinese AI lab Z.ai's latest model is a giant 754B parameter 1.51TB (on &lt;a href="https://huggingface.co/zai-org/GLM-5.1"&gt;Hugging Face&lt;/a&gt;) MIT-licensed monster - the same size as their previous GLM-5 release, and sharing the &lt;a href="https://huggingface.co/papers/2602.15763"&gt;same paper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's available &lt;a href="https://openrouter.ai/z-ai/glm-5.1"&gt;via OpenRouter&lt;/a&gt; so I asked it to draw me a pelican:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install llm-openrouter
llm -m openrouter/z-ai/glm-5.1 'Generate an SVG of a pelican on a bicycle'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And something new happened... unprompted, the model &lt;a href="https://gist.github.com/simonw/af7170f54256cc007ef28a8721564be8"&gt;decided to give me&lt;/a&gt; an HTML page that included both the SVG and a separate set of CSS animations!&lt;/p&gt;
&lt;p&gt;The SVG was excellent, and might be my new favorite from an open weights model:&lt;/p&gt;
&lt;p&gt;&lt;img alt="The bicycle is red and has a frame the correct shape and wheels with spokes. The pelican is a perky little fella." src="https://static.simonwillison.net/static/2026/glm-5.1-pelican.png" /&gt;&lt;/p&gt;
&lt;p&gt;But the animation &lt;a href="https://gisthost.github.io/?73bb6808b18c2482f66e5f082c75f36e"&gt;broke it&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animation - the wheels and pedals rotate, the clouds move... and the pelican has vanished, but there is a little blob bobbing up and down in the top left corner." src="https://static.simonwillison.net/static/2026/glm-5.1-broken-light-lossy.gif" /&gt;&lt;/p&gt;
&lt;p&gt;That's the pelican, floating up in the top left corner.&lt;/p&gt;
&lt;p&gt;I usually don't do follow-up prompts for the pelican test, but in this case I made an exception:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm -c 'the animation is a bit broken, the pelican ends up positioned off the screen at the top right'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GLM 5.1 replied:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The issue is that CSS &lt;code&gt;transform&lt;/code&gt; animations on SVG elements override the SVG &lt;code&gt;transform&lt;/code&gt; attribute used for positioning, causing the pelican to lose its placement and fly off to the top-right. The fix is to separate positioning (SVG attribute) from animation (inner group) and use &lt;code&gt;&amp;lt;animateTransform&amp;gt;&lt;/code&gt; for SVG rotations since it handles coordinate systems correctly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And spat out &lt;a href="https://static.simonwillison.net/static/2026/glm-5.1-pelican-fixed.html"&gt;fresh HTML&lt;/a&gt; which fixed the problem!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Now everything is right - the bicycle rotates correctly, the pelican sits on it and bobs up and down, and its lower beak moves slightly as well." src="https://static.simonwillison.net/static/2026/glm-5.1-pelican-fixed-medium-lossy.gif" /&gt;&lt;/p&gt;
&lt;p&gt;I particularly like the animation of the beak, which is described in the SVG comments like so:&lt;/p&gt;
&lt;div class="highlight highlight-text-xml-svg"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;&amp;lt;!--&lt;/span&gt; Pouch (lower beak) with wobble &lt;span class="pl-c"&gt;--&amp;gt;&lt;/span&gt;&lt;/span&gt;
&amp;lt;&lt;span class="pl-ent"&gt;g&lt;/span&gt;&amp;gt;
    &amp;lt;&lt;span class="pl-ent"&gt;path&lt;/span&gt; &lt;span class="pl-e"&gt;d&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;M42,-58 Q43,-50 48,-42 Q55,-35 62,-38 Q70,-42 75,-60 L42,-58 Z&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;fill&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;url(#pouchGrad)&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;stroke&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#b06008&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;stroke-width&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;opacity&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;0.9&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;/&amp;gt;
    &amp;lt;&lt;span class="pl-ent"&gt;path&lt;/span&gt; &lt;span class="pl-e"&gt;d&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;M48,-50 Q55,-46 60,-52&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;fill&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;none&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;stroke&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#c06a08&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;stroke-width&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;0.8&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;opacity&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;0.6&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;/&amp;gt;
    &amp;lt;&lt;span class="pl-ent"&gt;animateTransform&lt;/span&gt; &lt;span class="pl-e"&gt;attributeName&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;transform&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;type&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;scale&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-e"&gt;values&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1,1; 1.03,0.97; 1,1&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;dur&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;0.75s&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;repeatCount&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;indefinite&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-e"&gt;additive&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sum&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;/&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;g&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: On Bluesky &lt;a href="https://bsky.app/profile/charles.capps.me/post/3miwrn42mjc2t"&gt;@charles.capps.me suggested&lt;/a&gt; a "NORTH VIRGINIA OPOSSUM ON AN E-SCOOTER" and...&lt;/p&gt;
&lt;p&gt;&lt;img alt="This is so great. It's dark, the possum is clearly a possum, it's riding an escooter, lovely animation, tail bobbing up and down, caption says NORTH VIRGINIA OPOSSUM, CRUISING THE COMMONWEALTH SINCE DUSK - only glitch is that it occasionally blinks and the eyes fall off the face" src="https://static.simonwillison.net/static/2026/glm-possum-escooter.gif.gif" /&gt;&lt;/p&gt;
&lt;p&gt;The HTML+SVG comments on that one include &lt;code&gt;/* Earring sparkle */, &amp;lt;!-- Opossum fur gradient --&amp;gt;, &amp;lt;!-- Distant treeline silhouette - Virginia pines --&amp;gt;,  &amp;lt;!-- Front paw on handlebar --&amp;gt;&lt;/code&gt; - here's &lt;a href="https://gist.github.com/simonw/1864b89f5304eba03c3ded4697e156c4"&gt;the transcript&lt;/a&gt; and the &lt;a href="https://static.simonwillison.net/static/2026/glm-possum-escooter.html"&gt;HTML result&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/llm-release"&gt;llm-release&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/glm"&gt;glm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle"&gt;pelican-riding-a-bicycle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-in-china"&gt;ai-in-china&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/svg"&gt;svg&lt;/a&gt;&lt;/p&gt;



</summary><category term="llm-release"/><category term="generative-ai"/><category term="glm"/><category term="pelican-riding-a-bicycle"/><category term="ai"/><category term="ai-in-china"/><category term="llms"/><category term="css"/><category term="svg"/></entry><entry><title>Pretext</title><link href="https://simonwillison.net/2026/Mar/29/pretext/#atom-tag" rel="alternate"/><published>2026-03-29T20:08:45+00:00</published><updated>2026-03-29T20:08:45+00:00</updated><id>https://simonwillison.net/2026/Mar/29/pretext/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/chenglou/pretext"&gt;Pretext&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Exciting new browser library from Cheng Lou, previously a React core developer and the original creator of the &lt;a href="https://github.com/chenglou/react-motion"&gt;react-motion&lt;/a&gt; animation library.&lt;/p&gt;
&lt;p&gt;Pretext solves the problem of calculating the height of a paragraph of line-wrapped text &lt;em&gt;without touching the DOM&lt;/em&gt;. The usual way of doing this is to render the text and measure its dimensions, but this is extremely expensive. Pretext uses an array of clever tricks to make this much, much faster, which enables all sorts of new text rendering effects in browser applications.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://chenglou.me/pretext/dynamic-layout/"&gt;one demo&lt;/a&gt; that shows the kind of things this makes possible:&lt;/p&gt;
&lt;video autoplay loop muted playsinline
  poster="https://static.simonwillison.net/static/2026/pretex.jpg"&gt;
  &lt;source src="https://static.simonwillison.net/static/2026/pretex.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;The key to how this works is the way it separates calculations into a call to a &lt;code&gt;prepare()&lt;/code&gt; function followed by multiple calls to &lt;code&gt;layout()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;prepare()&lt;/code&gt; function splits the input text into segments (effectively words, but it can take things like soft hyphens and non-latin character sequences and emoji into account as well) and measures those using an off-screen canvas, then caches the results. This is comparatively expensive but only runs once.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;layout()&lt;/code&gt; function can then emulate the word-wrapping logic in browsers to figure out how many wrapped lines the text will occupy at a specified width and measure the overall height.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://claude.ai/share/7859cbe1-1350-4341-bb40-6aa241d6a1fe"&gt;had Claude&lt;/a&gt; build me &lt;a href="https://tools.simonwillison.net/pretext-explainer"&gt;this interactive artifact&lt;/a&gt; to help me visually understand what's going on, based on a simplified version of Pretext itself.&lt;/p&gt;
&lt;p&gt;The way this is tested is particularly impressive. The earlier tests &lt;a href="https://github.com/chenglou/pretext/commit/d07dd7a5008726f99a15cebe0abd9031022e28ef#diff-835c37ed3b9234ed4d90c7703addb8e47f4fee6d9a28481314afd15ac472f8d2"&gt;rendered a full copy of the Great Gatsby&lt;/a&gt; in multiple browsers to confirm that the estimated measurements were correct against a large volume of text. This was later joined by &lt;a href="https://github.com/chenglou/pretext/tree/main/corpora"&gt;the corpora/ folder&lt;/a&gt; using the same technique against lengthy public domain documents in Thai, Chinese, Korean, Japanese, Arabic, and more.&lt;/p&gt;
&lt;p&gt;Cheng Lou &lt;a href="https://twitter.com/_chenglou/status/2037715226838343871"&gt;says&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The engine’s tiny (few kbs), aware of browser quirks, supports all the languages you’ll need, including Korean mixed with RTL Arabic and platform-specific emojis&lt;/p&gt;
&lt;p&gt;This was achieved through showing Claude Code and Codex the browsers ground truth, and have them measure &amp;amp; iterate against those at every significant container width, running over weeks&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/_chenglou/status/2037713766205608234"&gt;@_chenglou&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/react"&gt;react&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/typescript"&gt;typescript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="react"/><category term="browsers"/><category term="javascript"/><category term="typescript"/><category term="testing"/></entry><entry><title>Quoting Eric Meyer</title><link href="https://simonwillison.net/2026/Feb/15/eric-meyer/#atom-tag" rel="alternate"/><published>2026-02-15T13:36:20+00:00</published><updated>2026-02-15T13:36:20+00:00</updated><id>https://simonwillison.net/2026/Feb/15/eric-meyer/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://mastodon.social/@Meyerweb/116065151451468199"&gt;&lt;p&gt;I saw yet another “CSS is a massively bloated mess” whine and I’m like.  My dude.  My brother in Chromium.  It is trying as hard as it can to express the totality of visual presentation and layout design and typography and animation and digital interactivity and a few other things in a human-readable text format.  It’s not bloated, it’s fantastically ambitious.  Its reach is greater than most of us can hope to grasp.  Put some &lt;em&gt;respect&lt;/em&gt; on its &lt;em&gt;name&lt;/em&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://mastodon.social/@Meyerweb/116065151451468199"&gt;Eric Meyer&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/eric-meyer"&gt;eric-meyer&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="web-standards"/><category term="eric-meyer"/></entry><entry><title>Launching Interop 2026</title><link href="https://simonwillison.net/2026/Feb/15/interop-2026/#atom-tag" rel="alternate"/><published>2026-02-15T04:33:22+00:00</published><updated>2026-02-15T04:33:22+00:00</updated><id>https://simonwillison.net/2026/Feb/15/interop-2026/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://hacks.mozilla.org/2026/02/launching-interop-2026/"&gt;Launching Interop 2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Jake Archibald reports on Interop 2026, the initiative between Apple, Google, Igalia, Microsoft, and Mozilla to collaborate on ensuring a targeted set of web platform features reach cross-browser parity over the course of the year.&lt;/p&gt;
&lt;p&gt;I hadn't realized how influential and successful the Interop series has been. It started back in 2021 as &lt;a href="https://web.dev/blog/compat2021"&gt;Compat 2021&lt;/a&gt; before being rebranded to Interop &lt;a href="https://blogs.windows.com/msedgedev/2022/03/03/microsoft-edge-and-interop-2022/"&gt;in 2022&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The dashboards for each year can be seen here, and they demonstrate how wildly effective the program has been: &lt;a href="https://wpt.fyi/interop-2021"&gt;2021&lt;/a&gt;, &lt;a href="https://wpt.fyi/interop-2022"&gt;2022&lt;/a&gt;, &lt;a href="https://wpt.fyi/interop-2023"&gt;2023&lt;/a&gt;, &lt;a href="https://wpt.fyi/interop-2024"&gt;2024&lt;/a&gt;, &lt;a href="https://wpt.fyi/interop-2025"&gt;2025&lt;/a&gt;, &lt;a href="https://wpt.fyi/interop-2026"&gt;2026&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's the progress chart for 2025, which shows every browser vendor racing towards a 95%+ score by the end of the year:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Line chart showing Interop 2025 browser compatibility scores over the year (Jan–Dec) for Chrome, Edge, Firefox, Safari, and Interop. Y-axis ranges from 0% to 100%. Chrome (yellow) and Edge (green) lead, starting around 80% and reaching near 100% by Dec. Firefox (orange) starts around 48% and climbs to ~98%. Safari (blue) starts around 45% and reaches ~96%. The Interop line (dark green/black) starts lowest around 29% and rises to ~95% by Dec. All browsers converge near 95–100% by year's end." src="https://static.simonwillison.net/static/2026/interop-2025.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The feature I'm most excited about in 2026 is &lt;a href="https://developer.mozilla.org/docs/Web/API/View_Transition_API/Using#basic_mpa_view_transition"&gt;Cross-document View Transitions&lt;/a&gt;, building on the successful 2025 target of &lt;a href="https://developer.mozilla.org/docs/Web/API/View_Transition_API/Using"&gt;Same-Document View Transitions&lt;/a&gt;. This will provide fancy SPA-style transitions between pages on websites with no JavaScript at all.&lt;/p&gt;
&lt;p&gt;As a keen WebAssembly tinkerer I'm also intrigued by this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md"&gt;JavaScript Promise Integration for Wasm&lt;/a&gt; allows WebAssembly to asynchronously 'suspend', waiting on the result of an external promise. This simplifies the compilation of languages like C/C++ which expect APIs to run synchronously.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jake-archibald"&gt;jake-archibald&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="browsers"/><category term="webassembly"/><category term="jake-archibald"/><category term="web-standards"/><category term="javascript"/></entry><entry><title>Quoting Adam Wathan</title><link href="https://simonwillison.net/2026/Jan/7/adam-wathan/#atom-tag" rel="alternate"/><published>2026-01-07T17:29:29+00:00</published><updated>2026-01-07T17:29:29+00:00</updated><id>https://simonwillison.net/2026/Jan/7/adam-wathan/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://github.com/tailwindlabs/tailwindcss.com/pull/2388#issuecomment-3717222957"&gt;&lt;p&gt;[...] the reality is that 75% of the people on our engineering team lost their jobs here yesterday because of the brutal impact AI has had on our business. And every second I spend trying to do fun free things for the community like this is a second I'm not spending trying to turn the business around and make sure the people who are still here are getting their paychecks every month. [...]&lt;/p&gt;
&lt;p&gt;Traffic to our docs is down about 40% from early 2023 despite Tailwind being more popular than ever. The docs are the only way people find out about our commercial products, and without customers we can't afford to maintain the framework. [...]&lt;/p&gt;
&lt;p&gt;Tailwind is growing faster than it ever has and is bigger than it ever has been, and our revenue is down close to 80%. Right now there's just no correlation between making Tailwind easier to use and making development of the framework more sustainable.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://github.com/tailwindlabs/tailwindcss.com/pull/2388#issuecomment-3717222957"&gt;Adam Wathan&lt;/a&gt;, CEO, Tailwind Labs&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai-ethics"/><category term="css"/><category term="generative-ai"/><category term="ai"/><category term="llms"/><category term="open-source"/></entry><entry><title>Dark mode</title><link href="https://simonwillison.net/2025/Dec/10/dark-mode/#atom-tag" rel="alternate"/><published>2025-12-10T16:05:34+00:00</published><updated>2025-12-10T16:05:34+00:00</updated><id>https://simonwillison.net/2025/Dec/10/dark-mode/#atom-tag</id><summary type="html">
    &lt;p&gt;I've never been particularly invested dark v.s. light mode but I get enough people complaining that this site is "blinding" that I decided to see if Claude Code for web could produce a useful dark mode from my existing CSS. It did &lt;a href="https://github.com/simonw/simonwillisonblog/pull/572/files"&gt;a decent job&lt;/a&gt;, using CSS properties, &lt;code&gt;@media (prefers-color-scheme: dark)&lt;/code&gt; and a &lt;code&gt;data-theme="dark"&lt;/code&gt; attribute based on this prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add a dark theme which is triggered by user media preferences but can also be switched on using localStorage - then put a little icon in the footer for toggling it between default auto, forced regular and forced dark mode&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The site defaults to picking up the user's preferences, but there's also a toggle in the footer which switches between auto, forced-light and forced-dark. Here's an animated demo:&lt;/p&gt;
&lt;p&gt;&lt;img alt="This site on mobile. Clicking the icon in the footer switches to a black background with readable text." src="https://static.simonwillison.net/static/2025/dark-mode.gif" /&gt;&lt;/p&gt;
&lt;p&gt;I had Claude Code &lt;a href="https://gistpreview.github.io/?5ea34de3e999bd32d0f86beef4bd803d"&gt;make me that GIF&lt;/a&gt; from two static screenshots - it used this ImageMagick recipe:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick -delay 300 -loop 0 one.png two.png \
    -colors 128 -layers Optimize dark-mode.gif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CSS ended up with some duplication due to the need to handle both the media preference and the explicit user selection. We &lt;a href="https://github.com/simonw/simonwillisonblog/commit/d4bc7573775960a630145a287d854b8569da6f72#diff-5acc582e2a25639d184d784747a69ff9b30061aca8d5913d9c7e67452e715e08"&gt;fixed that with Cog&lt;/a&gt;.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/design"&gt;design&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="design"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>No build frontend is so much more fun</title><link href="https://simonwillison.net/2025/May/31/no-build/#atom-tag" rel="alternate"/><published>2025-05-31T14:23:35+00:00</published><updated>2025-05-31T14:23:35+00:00</updated><id>https://simonwillison.net/2025/May/31/no-build/#atom-tag</id><summary type="html">
    &lt;p&gt;If you've found web development frustrating over the past 5-10 years, here's something that has worked worked great for me: give yourself permission to avoid any form of frontend build system (so no npm / React / TypeScript / JSX / Babel / Vite / Tailwind etc) and code in HTML and JavaScript like it's 2009.&lt;/p&gt;
&lt;p&gt;The joy came flooding back to me! It turns out browser APIs are really good now.&lt;/p&gt;
&lt;p&gt;You don't even need jQuery to paper over the gaps any more - use &lt;code&gt;document.querySelectorAll()&lt;/code&gt; and &lt;code&gt;fetch()&lt;/code&gt; directly and see how much value you can build with a few dozen lines of code.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-development"&gt;web-development&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/frontend"&gt;frontend&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="html"/><category term="javascript"/><category term="web-development"/><category term="frontend"/></entry><entry><title>CSS Minecraft</title><link href="https://simonwillison.net/2025/May/26/css-minecraft/#atom-tag" rel="alternate"/><published>2025-05-26T23:48:36+00:00</published><updated>2025-05-26T23:48:36+00:00</updated><id>https://simonwillison.net/2025/May/26/css-minecraft/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://benjaminaster.github.io/CSS-Minecraft/"&gt;CSS Minecraft&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Incredible project by Benjamin Aster:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There is no JavaScript on this page. All the logic is made 100% with pure HTML &amp;amp; CSS. For the best performance, please close other tabs and running programs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The page implements a full Minecraft-style world editor: you can place and remove blocks of 7 different types in a 9x9x9 world, and rotate that world in 3D to view it from different angles.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo. I start with a 9x9 green grid and add several blocks to it in different materials, rotating the screen with on-screen controls to see different angles." src="https://static.simonwillison.net/static/2025/minecraft-css.gif" /&gt;&lt;/p&gt;
&lt;p&gt;It's implemented in just &lt;a href="https://github.com/BenjaminAster/CSS-Minecraft/blob/main/main.css"&gt;480 lines of CSS&lt;/a&gt;... and 46,022 lines (3.07MB) of HTML!&lt;/p&gt;
&lt;p&gt;The key trick that gets this to work is &lt;strong&gt;labels&lt;/strong&gt; combined with the &lt;code&gt;has()&lt;/code&gt; selector. The page has 35,001 &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; elements and 5,840 &lt;code&gt;&amp;lt;input type="radio"&amp;gt;&lt;/code&gt; elements - those radio elements are the state storage engine. Clicking on any of the six visible faces of a cube is clicking on a label, and the &lt;code&gt;for=""&lt;/code&gt; of that label is the radio box for the neighboring cube in that dimension.&lt;/p&gt;
&lt;p&gt;When you switch materials you're actually switching the available visible labels:&lt;/p&gt;
&lt;pre&gt;.&lt;span class="pl-c1"&gt;controls&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;has&lt;/span&gt;(
  &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; .&lt;span class="pl-c1"&gt;block-chooser&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; .&lt;span class="pl-c1"&gt;stone&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-ent"&gt;input&lt;/span&gt;[&lt;span class="pl-c1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;radio&lt;/span&gt;]&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;checked&lt;/span&gt;
) &lt;span class="pl-c1"&gt;~&lt;/span&gt; &lt;span class="pl-ent"&gt;main&lt;/span&gt; .&lt;span class="pl-c1"&gt;cubes-container&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; .&lt;span class="pl-c1"&gt;cube&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;not&lt;/span&gt;(.&lt;span class="pl-c1"&gt;stone&lt;/span&gt;) {
  &lt;span class="pl-c1"&gt;display&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; none;
}&lt;/pre&gt;

&lt;p&gt;Claude Opus 4 &lt;a href="https://claude.ai/share/35ccb894-d26d-4698-b743-3de130adf433"&gt;explanation&lt;/a&gt;: "When the "stone" radio button is checked, all cube elements except those with the &lt;code&gt;.stone&lt;/code&gt; class are hidden (&lt;code&gt;display: none&lt;/code&gt;)".&lt;/p&gt;
&lt;p&gt;Here's a shortened version of the &lt;a href="https://pugjs.org/api/getting-started.html"&gt;Pug&lt;/a&gt; template (&lt;a href="https://github.com/BenjaminAster/CSS-Minecraft/blob/main/index.pug"&gt;full code here&lt;/a&gt;) which illustrates how the HTML structure works:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;//- pug index.pug -w&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;/span&gt;&lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;blocks&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; [&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;air&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;stone&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;grass&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;dirt&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;log&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;wood&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;leaves&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;glass&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;];&lt;/span&gt;
&lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;layers&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;9&lt;/span&gt;;&lt;/span&gt;
&lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;rows&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;9&lt;/span&gt;;&lt;/span&gt;
&lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;columns&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;9&lt;/span&gt;;&lt;/span&gt;
&amp;lt;&lt;span class="pl-ent"&gt;html&lt;/span&gt; &lt;span class="pl-e"&gt;lang&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;en&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;style&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-v"&gt;--layers&lt;/span&gt;: #{layers}; &lt;span class="pl-v"&gt;--rows&lt;/span&gt;: #{rows}; &lt;span class="pl-v"&gt;--columns&lt;/span&gt;: #{columns}&lt;/span&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&amp;lt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;blocks&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
  &lt;span class="pl-k"&gt;for&lt;/span&gt; _, layer &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;Array&lt;/span&gt;(layers)
    &lt;span class="pl-k"&gt;for&lt;/span&gt; _, row &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;Array&lt;/span&gt;(rows)
      &lt;span class="pl-k"&gt;for&lt;/span&gt; _, column &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;Array&lt;/span&gt;(columns)
        &amp;lt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cubes-container&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;style&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-v"&gt;--layer&lt;/span&gt;: #{layer}; &lt;span class="pl-v"&gt;--row&lt;/span&gt;: #{&lt;span class="pl-c1"&gt;row&lt;/span&gt;}; &lt;span class="pl-v"&gt;--column&lt;/span&gt;: #{&lt;span class="pl-c1"&gt;column&lt;/span&gt;}&lt;/span&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
          &lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;selectedBlock&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; layer &lt;span class="pl-k"&gt;===&lt;/span&gt; layers &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt; &lt;span class="pl-k"&gt;?&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;grass&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;air&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;;&lt;/span&gt;
          &lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;name&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;cube-layer-&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;layer&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;-row-&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;row&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;-column-&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;column&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;/span&gt;;&lt;/span&gt;
          &amp;lt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube #{blocks[0]}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
            &lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;name&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;-&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;blocks[&lt;span class="pl-c1"&gt;0&lt;/span&gt;]&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;/span&gt;;&lt;/span&gt;
            &amp;lt;&lt;span class="pl-ent"&gt;input&lt;/span&gt; &lt;span class="pl-e"&gt;type&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;radio&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;name&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{name}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;id&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;!{selectedBlock&lt;/span&gt; === &lt;span class="pl-e"&gt;blocks[0]&lt;/span&gt; &lt;span class="pl-e"&gt;?&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;checked&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-e"&gt;}&lt;/span&gt; /&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;front&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;back&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;left&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;right&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;top&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;bottom&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
          &amp;lt;/&lt;span class="pl-ent"&gt;div&lt;/span&gt;&amp;gt;
          &lt;span class="pl-k"&gt;each&lt;/span&gt; block, index &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-smi"&gt;blocks&lt;/span&gt;.&lt;span class="pl-c1"&gt;slice&lt;/span&gt;(&lt;span class="pl-c1"&gt;1&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;name&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;-&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;block&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;/span&gt;;&lt;/span&gt;
            &lt;span class="pl-s1"&gt;- &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;checked&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; index &lt;span class="pl-k"&gt;===&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;;&lt;/span&gt;
            &amp;lt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube #{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;input&lt;/span&gt; &lt;span class="pl-e"&gt;type&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;radio&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;name&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{name}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;id&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;#{id}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;!{selectedBlock&lt;/span&gt; === &lt;span class="pl-e"&gt;block&lt;/span&gt; &lt;span class="pl-e"&gt;?&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;checked&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-e"&gt;}&lt;/span&gt; /&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer}-row-#{row + 1}-column-#{column}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;front&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer}-row-#{row - 1}-column-#{column}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;back&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer}-row-#{row}-column-#{column + 1}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;left&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer}-row-#{row}-column-#{column - 1}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;right&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer - 1}-row-#{row}-column-#{column}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;top&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
              &amp;lt;&lt;span class="pl-ent"&gt;label&lt;/span&gt; &lt;span class="pl-e"&gt;for&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cube-layer-#{layer + 1}-row-#{row}-column-#{column}-#{block}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;bottom&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span class="pl-ent"&gt;label&lt;/span&gt;&amp;gt;
            &amp;lt;/&lt;span class="pl-ent"&gt;div&lt;/span&gt;&amp;gt;
&lt;span class="pl-c"&gt;          //- /each&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;/span&gt;        &amp;lt;/&lt;span class="pl-ent"&gt;div&lt;/span&gt;&amp;gt;
&lt;span class="pl-c"&gt;      //- /for&lt;/span&gt;
&lt;span class="pl-c"&gt;    //- /for&lt;/span&gt;
&lt;span class="pl-c"&gt;  //- /for&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;/span&gt;&amp;lt;/&lt;span class="pl-ent"&gt;div&lt;/span&gt;&amp;gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;So for every one of the 9x9x9 = 729 cubes there is a set of eight radio boxes sharing the same name such as &lt;code&gt;cube-layer-0-row-0-column-3&lt;/code&gt; - which means it can have one of eight values ("air" is clear space, the others are material types). There are six labels, one for each side of the cube - and those label &lt;code&gt;for=""&lt;/code&gt; attributes target the next block over of the current selected, visible material type.&lt;/p&gt;
&lt;p&gt;The other brilliant technique is the way it implements 3D viewing with controls for rotation and moving the viewport. The trick here relies on CSS animation:&lt;/p&gt;
&lt;pre&gt;.&lt;span class="pl-c1"&gt;controls&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;has&lt;/span&gt;(.&lt;span class="pl-c1"&gt;up&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;active&lt;/span&gt;) &lt;span class="pl-c1"&gt;~&lt;/span&gt; &lt;span class="pl-ent"&gt;main&lt;/span&gt; .&lt;span class="pl-c1"&gt;down&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation-play-state&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; running;
}
.&lt;span class="pl-c1"&gt;controls&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;has&lt;/span&gt;(.&lt;span class="pl-c1"&gt;down&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;active&lt;/span&gt;) &lt;span class="pl-c1"&gt;~&lt;/span&gt; &lt;span class="pl-ent"&gt;main&lt;/span&gt; .&lt;span class="pl-c1"&gt;up&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation-play-state&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; running;
}
.&lt;span class="pl-c1"&gt;controls&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;has&lt;/span&gt;(.&lt;span class="pl-c1"&gt;clockwise&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;active&lt;/span&gt;) &lt;span class="pl-c1"&gt;~&lt;/span&gt; &lt;span class="pl-ent"&gt;main&lt;/span&gt; .&lt;span class="pl-c1"&gt;clockwise&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation-play-state&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; running;
}
.&lt;span class="pl-c1"&gt;controls&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;has&lt;/span&gt;(.&lt;span class="pl-c1"&gt;counterclockwise&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;active&lt;/span&gt;) &lt;span class="pl-c1"&gt;~&lt;/span&gt; &lt;span class="pl-ent"&gt;main&lt;/span&gt; .&lt;span class="pl-c1"&gt;counterclockwise&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation-play-state&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; running;
}&lt;/pre&gt;

&lt;p&gt;Then later on there are animations defined for each of those different controls:&lt;/p&gt;
&lt;pre&gt;.&lt;span class="pl-c1"&gt;content&lt;/span&gt; .&lt;span class="pl-c1"&gt;clockwise&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--animation-duration&lt;/span&gt;) linear &lt;span class="pl-c1"&gt;1&lt;span class="pl-smi"&gt;ms&lt;/span&gt;&lt;/span&gt; paused rotate-clockwise;
}
&lt;span class="pl-k"&gt;@keyframes&lt;/span&gt; rotate-clockwise {
  &lt;span class="pl-k"&gt;from&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; y &lt;span class="pl-c1"&gt;0&lt;span class="pl-smi"&gt;turn&lt;/span&gt;&lt;/span&gt;;
  }
  &lt;span class="pl-k"&gt;to&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; y &lt;span class="pl-en"&gt;calc&lt;/span&gt;(&lt;span class="pl-c1"&gt;-1&lt;/span&gt; &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--max-rotation&lt;/span&gt;));
  }
}
.&lt;span class="pl-c1"&gt;content&lt;/span&gt; .&lt;span class="pl-c1"&gt;counterclockwise&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;animation&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--animation-duration&lt;/span&gt;) linear &lt;span class="pl-c1"&gt;1&lt;span class="pl-smi"&gt;ms&lt;/span&gt;&lt;/span&gt; paused rotate-counterclockwise;
}
&lt;span class="pl-k"&gt;@keyframes&lt;/span&gt; rotate-counterclockwise {
  &lt;span class="pl-k"&gt;from&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; y &lt;span class="pl-c1"&gt;0&lt;span class="pl-smi"&gt;turn&lt;/span&gt;&lt;/span&gt;;
  }
  &lt;span class="pl-k"&gt;to&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; y &lt;span class="pl-en"&gt;calc&lt;/span&gt;(&lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--max-rotation&lt;/span&gt;));
  }
}&lt;/pre&gt;

&lt;p&gt;Any time you hold the mouse down on one of the controls you switch the animation state out of &lt;code&gt;paused&lt;/code&gt; to &lt;code&gt;running&lt;/code&gt;, until you release that button again. As the animation runs it changes the various 3D transform properties applied to the selected element.&lt;/p&gt;
&lt;p&gt;It's &lt;em&gt;fiendishly&lt;/em&gt; clever, and actually quite elegant and readable once you figure out the core tricks it's using.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=44100148"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/minecraft"&gt;minecraft&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="minecraft"/><category term="html"/></entry><entry><title>Annotated Presentation Creator</title><link href="https://simonwillison.net/2025/May/15/annotated-presentation-creator/#atom-tag" rel="alternate"/><published>2025-05-15T14:41:55+00:00</published><updated>2025-05-15T14:41:55+00:00</updated><id>https://simonwillison.net/2025/May/15/annotated-presentation-creator/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/annotated-presentations"&gt;Annotated Presentation Creator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've released a new version of my tool for creating annotated presentations. I use this to turn slides from my talks into &lt;a href="https://simonwillison.net/2025/May/15/building-on-llms/"&gt;posts like this one&lt;/a&gt; - here are &lt;a href="https://simonwillison.net/tags/annotated-talks/"&gt;a bunch more examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wrote the first version &lt;a href="https://simonwillison.net/2023/Aug/6/annotated-presentations/"&gt;in August 2023&lt;/a&gt; making extensive use of ChatGPT and GPT-4. That older version can &lt;a href="https://til.simonwillison.net/tools/annotated-presentations"&gt;still be seen here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This new edition is a design refresh using Claude 3.7 Sonnet (thinking). I ran this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm \
  -f https://til.simonwillison.net/tools/annotated-presentations \
  -s 'Improve this tool by making it respnonsive for mobile, improving the styling' \
  -m claude-3.7-sonnet -o thinking 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That uses &lt;code&gt;-f&lt;/code&gt; to fetch the original HTML (which has embedded CSS and JavaScript in a single page, convenient for working with LLMs) as a prompt fragment, then applies the system prompt instructions "Improve this tool by making it respnonsive for mobile, improving the styling" (typo included).&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/8010fca527eb588f006f70850d7c37a3"&gt;the full transcript&lt;/a&gt; (generated using &lt;code&gt;llm logs -cue&lt;/code&gt;) and &lt;a href="https://gist.github.com/simonw/70e1bdbf71fd53ba89922067d3401a3b/revisions#diff-b6337e5018b8ad3d751d42ddc4bc6c1a0328190c7e7cbfeb88321142aad8f31d"&gt;a diff&lt;/a&gt; illustrating the changes. Total cost 10.7781 cents.&lt;/p&gt;
&lt;p&gt;There was one visual glitch: the slides were distorted like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="The slide is distorted by being too high for its width" src="https://static.simonwillison.net/static/2025/bug.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I decided to try o4-mini to see if it could spot the problem (after &lt;a href="https://github.com/simonw/llm/issues/1037"&gt;fixing this LLM bug&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm o4-mini \
  -a bug.png \
  -f https://tools.simonwillison.net/annotated-presentations \
  -s 'Suggest a minimal fix for this distorted image'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It suggested adding &lt;code&gt;align-items: flex-start;&lt;/code&gt; to my &lt;code&gt;.bundle&lt;/code&gt; class (it quoted the &lt;code&gt;@media (min-width: 768px)&lt;/code&gt; bit but the solution was to add it to &lt;code&gt;.bundle&lt;/code&gt; at the top level), which fixed the bug.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of an &amp;quot;Annotated Presentation Creator&amp;quot; web application. The interface shows: &amp;quot;Annotated Presentation Creator&amp;quot; header, &amp;quot;Create beautiful annotated slides for your presentations. See How I make annotated presentations for instructions.&amp;quot; Below is an upload area with buttons &amp;quot;Choose Images&amp;quot;, &amp;quot;Load Images&amp;quot;, &amp;quot;Restore 64 saved items&amp;quot;, and &amp;quot;OCR Missing Alt Text&amp;quot;. The main area displays a presentation slide with &amp;quot;Building software on top of Large Language Models&amp;quot; by &amp;quot;Simon Willison - PyCon US 2025&amp;quot; dated &amp;quot;15th May 2025&amp;quot;, alongside an alt text input field and annotation section containing &amp;quot;The full handout for the workshop parts of this talk can be found at building-with-llms-pycon-2025.readthedocs.io.&amp;quot;" src="https://static.simonwillison.net/static/2025/annotated-updated.jpg" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-talks"&gt;annotated-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;



</summary><category term="claude"/><category term="openai"/><category term="annotated-talks"/><category term="ai"/><category term="llms"/><category term="vibe-coding"/><category term="tools"/><category term="generative-ai"/><category term="css"/><category term="ai-assisted-programming"/></entry><entry><title>Default styles for h1 elements are changing</title><link href="https://simonwillison.net/2025/Apr/11/default-styles-for-h1/#atom-tag" rel="alternate"/><published>2025-04-11T03:54:43+00:00</published><updated>2025-04-11T03:54:43+00:00</updated><id>https://simonwillison.net/2025/Apr/11/default-styles-for-h1/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.mozilla.org/en-US/blog/h1-element-styles/"&gt;Default styles for h1 elements are changing&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Wow, this is a rare occurrence! Firefox are rolling out a change to the default user-agent stylesheet for nested &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; elements, currently ramping from 5% to 50% of users and with full roll-out planned for Firefox 140 in June 2025. Chrome is showing deprecation warnings and Safari are expected to follow suit in the future.&lt;/p&gt;
&lt;p&gt;What's changing? The default sizes of &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; elements that are nested inside &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These are the default styles being removed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;/* where x is :is(article, aside, nav, section) */&lt;/span&gt;
&lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;h1&lt;/span&gt; { &lt;span class="pl-c1"&gt;margin-block&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0.83&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.50&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; }
&lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;h1&lt;/span&gt; { &lt;span class="pl-c1"&gt;margin-block&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.00&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.17&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; }
&lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;h1&lt;/span&gt; { &lt;span class="pl-c1"&gt;margin-block&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.33&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.00&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; }
&lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;h1&lt;/span&gt; { &lt;span class="pl-c1"&gt;margin-block&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1.67&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0.83&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; }
&lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;x&lt;/span&gt; &lt;span class="pl-ent"&gt;h1&lt;/span&gt; { &lt;span class="pl-c1"&gt;margin-block&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;2.33&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0.67&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;; }&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;The short version is that, many years ago, the HTML spec introduced the idea that an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; within a nested section should have the same meaning (and hence visual styling) as an &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;. This never really took off and wasn't reflected by the accessibility tree, and was removed from the HTML spec in 2022. The browsers are now trying to cleanup the legacy default styles.&lt;/p&gt;
&lt;p&gt;This advice from that post sounds sensible to me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Do not&lt;/strong&gt; rely on default browser styles for conveying a heading hierarchy. Explicitly define your document hierarchy using &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; for second-level headings, &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; for third-level, etc.&lt;/li&gt;
&lt;li&gt;Always define your own &lt;code&gt;font-size&lt;/code&gt; and &lt;code&gt;margin&lt;/code&gt; for &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43649853"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/safari"&gt;safari&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="html"/><category term="firefox"/><category term="mozilla"/><category term="browsers"/><category term="safari"/><category term="chrome"/><category term="web-standards"/></entry><entry><title>First look at the modern attr()</title><link href="https://simonwillison.net/2025/Apr/3/first-look-at-the-modern-attr/#atom-tag" rel="alternate"/><published>2025-04-03T15:53:52+00:00</published><updated>2025-04-03T15:53:52+00:00</updated><id>https://simonwillison.net/2025/Apr/3/first-look-at-the-modern-attr/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://ishadeed.com/article/modern-attr/"&gt;First look at the modern attr()&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Chrome 133 (released February 25th 2025) was the first browser to &lt;a href="https://developer.chrome.com/release-notes/133?hl=en#css_advanced_attr_function"&gt;ship support&lt;/a&gt; for the advanced CSS &lt;code&gt;attr()&lt;/code&gt; function (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/attr"&gt;MDN&lt;/a&gt;), which lets &lt;code&gt;attr()&lt;/code&gt; be used to compose values using types other than strings.&lt;/p&gt;
&lt;p&gt;Ahmad Shadeed explores potential applications of this in detail, trying it out for CSS grid columns, progress bars, background images, animation delays and more.&lt;/p&gt;
&lt;p&gt;I like this example that uses the &lt;code&gt;rows="5"&lt;/code&gt; attribute on a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; to calculate its &lt;code&gt;max-height&lt;/code&gt; - here wrapped in a feature detection block:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;@supports&lt;/span&gt; (&lt;span class="pl-c1"&gt;x&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;attr&lt;/span&gt;(x &lt;span class="pl-en"&gt;type&lt;/span&gt;(&lt;span class="pl-c1"&gt;*&lt;/span&gt;))) {
  &lt;span class="pl-ent"&gt;textarea&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;min-height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;calc&lt;/span&gt;(
      &lt;span class="pl-en"&gt;attr&lt;/span&gt;(rows &lt;span class="pl-en"&gt;type&lt;/span&gt;(&amp;lt;number&lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt;)) &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;50&lt;span class="pl-smi"&gt;px&lt;/span&gt;&lt;/span&gt;
    );
  }
}&lt;/pre&gt;

&lt;p&gt;That &lt;code&gt;type(&amp;lt;number&amp;gt;)&lt;/code&gt; is the new syntax.&lt;/p&gt;
&lt;p&gt;Many of Ahmad's examples can be achieved today across all browsers using a slightly more verbose CSS custom property syntax.&lt;/p&gt;
&lt;p&gt;Here are the tracking issues for CSS values support in &lt;code&gt;attr()&lt;/code&gt; for &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=435426"&gt;Firefox&lt;/a&gt; (opened 17 years ago) and &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=26609"&gt;WebKit&lt;/a&gt; (16 years ago).


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css-custom-properties"&gt;css-custom-properties&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ahmad-shadeed"&gt;ahmad-shadeed&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="css-custom-properties"/><category term="chrome"/><category term="web-standards"/><category term="ahmad-shadeed"/></entry><entry><title>Minimal CSS-only blurry image placeholders</title><link href="https://simonwillison.net/2025/Apr/3/minimal-css-only-blurry-image-placeholders/#atom-tag" rel="alternate"/><published>2025-04-03T02:44:18+00:00</published><updated>2025-04-03T02:44:18+00:00</updated><id>https://simonwillison.net/2025/Apr/3/minimal-css-only-blurry-image-placeholders/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://leanrada.com/notes/css-only-lqip/"&gt;Minimal CSS-only blurry image placeholders&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Absolutely brilliant piece of CSS ingenuity by Lean Rada, who describes a way to implement blurry placeholder images using just CSS, with syntax like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;img&lt;/span&gt; &lt;span class="pl-c1"&gt;src&lt;/span&gt;="&lt;span class="pl-s"&gt;…&lt;/span&gt;" &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;--lqip:192900&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;That 192900 number encodes everything needed to construct the placeholder - it manages to embed a single base color and six brightness components (in a 3x2 grid) in 20 bits, then encodes those as an integer in the roughly 2 million available values between -999,999 and 999,999 - beyond which range Lean found some browsers would start to lose precision.&lt;/p&gt;
&lt;p&gt;The implementation for decoding that value becomes a bunch of clever bit-fiddling CSS expressions to expand it into further CSS variables:&lt;/p&gt;
&lt;pre&gt;[&lt;span class="pl-c1"&gt;style&lt;/span&gt;&lt;span class="pl-c1"&gt;*=&lt;/span&gt;&lt;span class="pl-s"&gt;"--lqip:"&lt;/span&gt;] {
  &lt;span class="pl-s1"&gt;--lqip-ca&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;mod&lt;/span&gt;(&lt;span class="pl-en"&gt;round&lt;/span&gt;(down&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;calc&lt;/span&gt;((&lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip&lt;/span&gt;) &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-en"&gt;pow&lt;/span&gt;(&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;19&lt;/span&gt;)) &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-en"&gt;pow&lt;/span&gt;(&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;18&lt;/span&gt;)))&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;4&lt;/span&gt;);
  &lt;span class="pl-s1"&gt;--lqip-cb&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;mod&lt;/span&gt;(&lt;span class="pl-en"&gt;round&lt;/span&gt;(down&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;calc&lt;/span&gt;((&lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip&lt;/span&gt;) &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-en"&gt;pow&lt;/span&gt;(&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;19&lt;/span&gt;)) &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-en"&gt;pow&lt;/span&gt;(&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;16&lt;/span&gt;)))&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;4&lt;/span&gt;);
  &lt;span class="pl-c"&gt;/* more like that */&lt;/span&gt;
}&lt;/pre&gt;

&lt;p&gt;Which are expanded to even more variables with code like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;--lqip-ca-clr&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;hsl&lt;/span&gt;(&lt;span class="pl-c1"&gt;0&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-en"&gt;calc&lt;/span&gt;(&lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-ca&lt;/span&gt;) &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;));
&lt;span class="pl-s1"&gt;--lqip-cb-clr&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;hsl&lt;/span&gt;(&lt;span class="pl-c1"&gt;0&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-en"&gt;calc&lt;/span&gt;(&lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-cb&lt;/span&gt;) &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;));&lt;/pre&gt;

&lt;p&gt;And finally rendered using a CSS gradient definition that starts like this:&lt;/p&gt;
&lt;pre&gt;[&lt;span class="pl-c1"&gt;style&lt;/span&gt;&lt;span class="pl-c1"&gt;*=&lt;/span&gt;&lt;span class="pl-s"&gt;"--lqip:"&lt;/span&gt;] {
  &lt;span class="pl-c1"&gt;background-image&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt;
    &lt;span class="pl-en"&gt;radial-gradient&lt;/span&gt;(&lt;span class="pl-c1"&gt;50&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;75&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; at &lt;span class="pl-c1"&gt;16.67&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;25&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-ca-clr&lt;/span&gt;)&lt;span class="pl-kos"&gt;,&lt;/span&gt; transparent)&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-en"&gt;radial-gradient&lt;/span&gt;(&lt;span class="pl-c1"&gt;50&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;75&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; at &lt;span class="pl-c1"&gt;50&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;25&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-cb-clr&lt;/span&gt;)&lt;span class="pl-kos"&gt;,&lt;/span&gt; transparent)&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;/* ... */&lt;/span&gt;
    &lt;span class="pl-en"&gt;linear-gradient&lt;/span&gt;(&lt;span class="pl-c1"&gt;0&lt;span class="pl-smi"&gt;deg&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-base-clr&lt;/span&gt;)&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--lqip-base-clr&lt;/span&gt;));
}&lt;/pre&gt;

&lt;p&gt;The article includes several interactive explainers (most of which are also powered by pure CSS) illustrating how it all works.&lt;/p&gt;
&lt;p&gt;Their &lt;a href="https://github.com/Kalabasa/leanrada.com/blob/7b6739c7c30c66c771fcbc9e1dc8942e628c5024/main/scripts/update/lqip.mjs#L118-L159"&gt;Node.js script&lt;/a&gt; for converting images to these magic integers uses &lt;a href="https://www.npmjs.com/package/sharp"&gt;Sharp&lt;/a&gt; to resize the image to 3x2 and then use the &lt;a href="https://en.m.wikipedia.org/wiki/Oklab_color_space"&gt;Oklab perceptually uniform color space&lt;/a&gt; (new to me, that was created by Björn Ottosson in 2020) to derive the six resulting values.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43523220"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css-custom-properties"&gt;css-custom-properties&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="css-custom-properties"/></entry><entry><title>Backstory on the default styles for the HTML dialog modal</title><link href="https://simonwillison.net/2025/Mar/16/backstory/#atom-tag" rel="alternate"/><published>2025-03-16T16:36:36+00:00</published><updated>2025-03-16T16:36:36+00:00</updated><id>https://simonwillison.net/2025/Mar/16/backstory/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=43378225#43380129"&gt;Backstory on the default styles for the HTML dialog modal&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My TIL about &lt;a href="https://til.simonwillison.net/css/dialog-full-height"&gt;Styling an HTML dialog modal to take the full height of the viewport&lt;/a&gt; (here's the &lt;a href="https://tools.simonwillison.net/side-panel-dialog"&gt;interactive demo&lt;/a&gt;) showed up &lt;a href="https://news.ycombinator.com/item?id=43378225"&gt;on Hacker News&lt;/a&gt; this morning, and attracted this fascinating comment from Chromium engineer Ian Kilpatrick.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There's quite a bit of history here, but the abbreviated version is that the dialog element was originally added as a replacement for window.alert(), and there were a libraries polyfilling dialog and being surprisingly widely used.&lt;/p&gt;
&lt;p&gt;The mechanism which dialog was originally positioned was relatively complex, and slightly hacky (magic values for the insets).&lt;/p&gt;
&lt;p&gt;Changing the behaviour basically meant that we had to add "overflow:auto", and some form of "max-height"/"max-width" to ensure that the content within the dialog was actually reachable.&lt;/p&gt;
&lt;p&gt;The better solution to this was to add "max-height:stretch", "max-width:stretch". You can see &lt;a href="https://github.com/whatwg/html/pull/5936#discussion_r513642207"&gt;the discussion for this here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The problem is that no browser had (and still has) shipped the "stretch" keyword. (Blink &lt;a href="https://groups.google.com/a/chromium.org/g/blink-dev/c/SiZ2nDt3B9E/m/kP_rKOaDAgAJ?pli=1"&gt;likely will "soon"&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;However this was pushed back against as this had to go in a specification - and nobody implemented it ("-webit-fill-available" would have been an acceptable substitute in Blink but other browsers didn't have this working the same yet).&lt;/p&gt;
&lt;p&gt;Hence the calc() variant. (Primarily because of "box-sizing:content-box" being the default, and pre-existing border/padding styles on dialog that we didn't want to touch). [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I particularly enjoyed this insight into the challenges of evolving the standards that underlie the web, even for something this small:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One thing to keep in mind is that any changes that changes web behaviour is under some time pressure. If you leave something too long, sites will start relying on the previous behaviour - so it would have been arguably worse not to have done anything.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also from the comments I learned that Firefox DevTools &lt;em&gt;can&lt;/em&gt; show you user-agent styles, but that option is turned off by default - &lt;a href="https://til.simonwillison.net/css/dialog-full-height#user-content-update-firefox-can-show-browser-styles"&gt;notes on that here&lt;/a&gt;. Once I turned this option on I saw references to an &lt;code&gt;html.css&lt;/code&gt; stylesheet, so I dug around and &lt;a href="https://searchfox.org/mozilla-central/source/layout/style/res/html.css"&gt;found that in the Firefox source code&lt;/a&gt;. Here's &lt;a href="https://github.com/mozilla/gecko-dev/commits/HEAD/layout/style/res/html.css"&gt;the commit history&lt;/a&gt; for that file on the official GitHub mirror, which provides a detailed history of how Firefox default HTML styles have evolved with the standards over time.&lt;/p&gt;
&lt;p&gt;And &lt;a href="https://news.ycombinator.com/item?id=43378225#43380255"&gt;via uallo&lt;/a&gt; here are the same default HTML styles for other browsers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chromium: &lt;a href="https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/resources/html.css"&gt;third_party/blink/renderer/core/html/resources/html.css&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;WebKit: &lt;a href="https://github.com/WebKit/WebKit/blob/main/Source/WebCore/css/html.css"&gt;Source/WebCore/css/html.css&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="web-standards"/><category term="html"/><category term="chrome"/><category term="firefox"/></entry><entry><title>TIL: Styling an HTML dialog modal to take the full height of the viewport</title><link href="https://simonwillison.net/2025/Mar/14/styling-an-html-dialog/#atom-tag" rel="alternate"/><published>2025-03-14T23:13:55+00:00</published><updated>2025-03-14T23:13:55+00:00</updated><id>https://simonwillison.net/2025/Mar/14/styling-an-html-dialog/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/css/dialog-full-height"&gt;TIL: Styling an HTML dialog modal to take the full height of the viewport&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I spent some time today trying to figure out how to have a modal &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element present as a full height side panel that animates in from the side. The full height bit was hard, until Natalie helped me figure out that browsers apply a default &lt;code&gt;max-height: calc(100% - 6px - 2em);&lt;/code&gt; rule which needs to be over-ridden.&lt;/p&gt;
&lt;p&gt;Also included: some &lt;a href="https://til.simonwillison.net/css/dialog-full-height#user-content-spelunking-through-the-html-specification"&gt;spelunking through the HTML spec&lt;/a&gt; to figure out where that &lt;code&gt;calc()&lt;/code&gt; expression was first introduced. The answer was &lt;a href="https://github.com/whatwg/html/commit/979af1532"&gt;November 2020&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/natalie-downe"&gt;natalie-downe&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="til"/><category term="html"/><category term="natalie-downe"/><category term="web-standards"/></entry><entry><title>Building Websites With Lots of Little HTML Pages</title><link href="https://simonwillison.net/2025/Mar/10/building-websites-with-llms/#atom-tag" rel="alternate"/><published>2025-03-10T00:38:32+00:00</published><updated>2025-03-10T00:38:32+00:00</updated><id>https://simonwillison.net/2025/Mar/10/building-websites-with-llms/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.jim-nielsen.com/2025/lots-of-little-html-pages/"&gt;Building Websites With Lots of Little HTML Pages&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Jim Nielsen coins a confusing new acronym - LLMS for (L)ots of (L)ittle ht(M)l page(S). He's using this to describe his latest site refresh which makes extensive use of &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions/cross-document"&gt;cross-document view transitions&lt;/a&gt; - a fabulous new progressive enhancement CSS technique that's &lt;a href="https://caniuse.com/view-transitions"&gt;supported&lt;/a&gt; in Chrome and Safari (and hopefully soon &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823896"&gt;in Firefox&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With cross-document view transitions getting broader and broader support, I’m realizing that building in-page, progressively-enhanced interactions is more work than simply building two HTML pages and linking them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Jim now has small static pages powering his home page filtering interface and even his navigation menu, with CSS view transitions configured to smoothly animate between the pages. I think it feels really good - here's what it looked like for me in Chrome (it looked the same both with and without JavaScript disabled):&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo - clicking the tabs to swap between Latest, Trending and Hacker News Hits rearranges the list of posts in a smooth animation, then navigating to a post causes its title to enlarge and move to the top while the rest of the article loads in." src="https://static.simonwillison.net/static/2025/llms-demo.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Watching the network panel in my browser, most of these pages are 17-20KB gzipped (~45KB after they've decompressed). No wonder it feels so snappy.&lt;/p&gt;
&lt;p&gt;I poked around &lt;a href="https://blog.jim-nielsen.com/styles.css"&gt;in Jim's CSS&lt;/a&gt; and found this relevant code:&lt;/p&gt;
&lt;div class="highlight highlight-source-css"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;@view-transition&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;navigation&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; auto;
}

.&lt;span class="pl-c1"&gt;posts-nav&lt;/span&gt; &lt;span class="pl-ent"&gt;a&lt;/span&gt;[&lt;span class="pl-c1"&gt;aria-current&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"page"&lt;/span&gt;]&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;not&lt;/span&gt;(&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;last-child&lt;/span&gt;)&lt;span class="pl-kos"&gt;:&lt;/span&gt;&lt;span class="pl-c1"&gt;after&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;border-color&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-en"&gt;var&lt;/span&gt;(&lt;span class="pl-s1"&gt;--c-text&lt;/span&gt;);
  &lt;span class="pl-c1"&gt;view-transition-name&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; posts-nav;
}

&lt;span class="pl-c"&gt;/* Old stuff going out */&lt;/span&gt;
::&lt;span class="pl-c1"&gt;view-transition-old&lt;/span&gt;(&lt;span class="pl-ent"&gt;posts-nav&lt;/span&gt;) {
  &lt;span class="pl-c1"&gt;animation&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; fade &lt;span class="pl-c1"&gt;0.2&lt;span class="pl-smi"&gt;s&lt;/span&gt;&lt;/span&gt; linear forwards;
  &lt;span class="pl-c"&gt;/* &lt;a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/"&gt;https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/&lt;/a&gt; */&lt;/span&gt;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class="pl-c"&gt;/* New stuff coming in */&lt;/span&gt;
::&lt;span class="pl-c1"&gt;view-transition-new&lt;/span&gt;(&lt;span class="pl-ent"&gt;posts-nav&lt;/span&gt;) {
  &lt;span class="pl-c1"&gt;animation&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; fade &lt;span class="pl-c1"&gt;0.3&lt;span class="pl-smi"&gt;s&lt;/span&gt;&lt;/span&gt; linear reverse;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class="pl-k"&gt;@keyframes&lt;/span&gt; fade {
  &lt;span class="pl-k"&gt;from&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;opacity&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;;
  }
  &lt;span class="pl-k"&gt;to&lt;/span&gt; {
    &lt;span class="pl-c1"&gt;opacity&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;;
  }
}&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Jim observes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This really feels like a game-changer for simple sites. If you can keep your site simple, it’s easier to build traditional, JavaScript-powered on-page interactions as small, linked HTML pages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've experimented with view transitions for &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; in the past and the results were very promising. Maybe I'll pick that up again.&lt;/p&gt;
&lt;p&gt;Bonus: Jim has a &lt;a href="https://lobste.rs/s/csr4mw/building_websites_with_lots_little_html#c_ncxssq"&gt;clever JavaScript trick&lt;/a&gt; to avoid clicks to the navigation menu being added to the browser's history in the default case.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/csr4mw/building_websites_with_lots_little_html"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/view-transitions"&gt;view-transitions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/progressive-enhancement"&gt;progressive-enhancement&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="view-transitions"/><category term="progressive-enhancement"/></entry><entry><title>Clay UI library</title><link href="https://simonwillison.net/2024/Dec/21/clay-ui-library/#atom-tag" rel="alternate"/><published>2024-12-21T23:12:17+00:00</published><updated>2024-12-21T23:12:17+00:00</updated><id>https://simonwillison.net/2024/Dec/21/clay-ui-library/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.nicbarker.com/clay"&gt;Clay UI library&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fascinating project by Nic Barker, who describes Clay like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Clay is a flex-box style UI auto layout library in C, with declarative syntax and microsecond performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His &lt;a href="https://www.youtube.com/watch?v=DYWTw19_8r4"&gt;intro video&lt;/a&gt; to the library is outstanding: I learned a ton about how UI layout works from this, and the animated visual explanations are clear, tasteful and really helped land the different concepts:&lt;/p&gt;
&lt;p&gt;&lt;lite-youtube videoid="DYWTw19_8r4"
  title="Introducing Clay - High Performance UI Layout in C"
  playlabel="Play: Introducing Clay - High Performance UI Layout in C"
&gt; &lt;/lite-youtube&gt;&lt;/p&gt;

&lt;p&gt;Clay is a C library delivered in a single ~2000 line &lt;a href="https://github.com/nicbarker/clay/blob/main/clay.h"&gt;clay.h&lt;/a&gt; dependency-free header file. It only handles layout calculations: if you want to render the result you need to add an additional rendering layer.&lt;/p&gt;
&lt;p&gt;In a fascinating demo of the library, the &lt;a href="https://www.nicbarker.com/clay"&gt;Clay site itself&lt;/a&gt; is rendered using Clay C compiled to WebAssembly! You can even switch between the default HTML renderer and an alternative based on Canvas.&lt;/p&gt;
&lt;p&gt;This isn't necessarily a great idea: because the layout is entirely handled using &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; elements positioned using &lt;code&gt;transform: translate(0px, 70px)&lt;/code&gt; style CSS attempting to select text across multiple boxes behaves strangely, and it's not clear to me what the accessibility implications are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: &lt;a href="https://toot.cafe/@matt/113693374074675126"&gt;Matt Campbell&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The accessibility implications are as serious as you might guess. The links aren't properly labeled, there's no semantic markup such as headings, and since there's a div for every line, continuous reading with a screen reader is choppy, that is, it pauses at the end of every physical line.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It does make for a very compelling demo of what Clay is capable of though, especially when you resize your browser window and the page layout is recalculated in real-time via the Clay WebAssembly bridge.&lt;/p&gt;
&lt;p&gt;You can hit "D" on the website and open up a custom Clay debugger showing the hierarchy of layout elements on the page:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Clay website on the left, on the right is a panel showing a tree of UI layout elements, one has been selected and is showing details in a box at the bottom of the panel: Bounding Box: { x: 278, y: 13, width: 101, height: 24}, Layout Direction: LEFT_TO_RIGHT, Sizing: width: FITQ, height: FITQ, Padding: {x:8,uy:0}" src="https://static.simonwillison.net/static/2024/clay-debug.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;This also means that the entire page is defined using C code! Given that, I find the code itself &lt;a href="https://github.com/nicbarker/clay/blob/35d72e5fba6872be48d15ed9d84269a86cd72b4e/examples/clay-official-website/main.c#L124-L139"&gt;surprisingly readable&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-c"&gt;&lt;pre&gt;&lt;span class="pl-smi"&gt;void&lt;/span&gt; &lt;span class="pl-en"&gt;DeclarativeSyntaxPageDesktop&lt;/span&gt;() {
  &lt;span class="pl-en"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxPageDesktop"&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_GROW&lt;/span&gt;(), &lt;span class="pl-en"&gt;CLAY_SIZING_FIT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;min&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;windowHeight&lt;/span&gt; &lt;span class="pl-c1"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;50&lt;/span&gt; }) }, .&lt;span class="pl-s1"&gt;childAlignment&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {&lt;span class="pl-c1"&gt;0&lt;/span&gt;, &lt;span class="pl-c1"&gt;CLAY_ALIGN_Y_CENTER&lt;/span&gt;}, .&lt;span class="pl-s1"&gt;padding&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {.&lt;span class="pl-s1"&gt;x&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;50&lt;/span&gt;} })) {
    &lt;span class="pl-c1"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxPage"&lt;/span&gt;), &lt;span class="pl-c1"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_GROW&lt;/span&gt;(), &lt;span class="pl-en"&gt;CLAY_SIZING_GROW&lt;/span&gt;() }, .&lt;span class="pl-s1"&gt;childAlignment&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-c1"&gt;0&lt;/span&gt;, &lt;span class="pl-c1"&gt;CLAY_ALIGN_Y_CENTER&lt;/span&gt; }, .&lt;span class="pl-s1"&gt;padding&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-c1"&gt;32&lt;/span&gt;, &lt;span class="pl-c1"&gt;32&lt;/span&gt; }, .&lt;span class="pl-s1"&gt;childGap&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;32&lt;/span&gt; }), &lt;span class="pl-en"&gt;CLAY_BORDER&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;left&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; }, .&lt;span class="pl-s1"&gt;right&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; } })) {
      &lt;span class="pl-c1"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxPageLeftText"&lt;/span&gt;), &lt;span class="pl-c1"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_PERCENT&lt;/span&gt;(&lt;span class="pl-c1"&gt;0.5&lt;/span&gt;) }, .&lt;span class="pl-c1"&gt;layoutDirection&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;CLAY_TOP_TO_BOTTOM&lt;/span&gt;, .&lt;span class="pl-c1"&gt;childGap&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;8&lt;/span&gt; })) {
        &lt;span class="pl-en"&gt;CLAY_TEXT&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_STRING&lt;/span&gt;(&lt;span class="pl-s"&gt;"Declarative Syntax"&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_TEXT_CONFIG&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;fontSize&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;52&lt;/span&gt;, .&lt;span class="pl-c1"&gt;fontId&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;FONT_ID_TITLE_56&lt;/span&gt;, .&lt;span class="pl-c1"&gt;textColor&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; }));
        &lt;span class="pl-en"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxSpacer"&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_GROW&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;max&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;16&lt;/span&gt; }) } })) {}
        &lt;span class="pl-en"&gt;CLAY_TEXT&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_STRING&lt;/span&gt;(&lt;span class="pl-s"&gt;"Flexible and readable declarative syntax with nested UI element hierarchies."&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_TEXT_CONFIG&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;fontSize&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;28&lt;/span&gt;, .&lt;span class="pl-c1"&gt;fontId&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;FONT_ID_BODY_36&lt;/span&gt;, .&lt;span class="pl-c1"&gt;textColor&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; }));
        &lt;span class="pl-en"&gt;CLAY_TEXT&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_STRING&lt;/span&gt;(&lt;span class="pl-s"&gt;"Mix elements with standard C code like loops, conditionals and functions."&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_TEXT_CONFIG&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;fontSize&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;28&lt;/span&gt;, .&lt;span class="pl-c1"&gt;fontId&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;FONT_ID_BODY_36&lt;/span&gt;, .&lt;span class="pl-c1"&gt;textColor&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; }));
        &lt;span class="pl-en"&gt;CLAY_TEXT&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_STRING&lt;/span&gt;(&lt;span class="pl-s"&gt;"Create your own library of re-usable components from UI primitives like text, images and rectangles."&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_TEXT_CONFIG&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;fontSize&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;28&lt;/span&gt;, .&lt;span class="pl-c1"&gt;fontId&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;FONT_ID_BODY_36&lt;/span&gt;, .&lt;span class="pl-c1"&gt;textColor&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;COLOR_RED&lt;/span&gt; }));
      }
      &lt;span class="pl-en"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxPageRightImage"&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_PERCENT&lt;/span&gt;(&lt;span class="pl-c1"&gt;0.50&lt;/span&gt;) }, .&lt;span class="pl-c1"&gt;childAlignment&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {.&lt;span class="pl-s1"&gt;x&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;CLAY_ALIGN_X_CENTER&lt;/span&gt;} })) {
        &lt;span class="pl-c1"&gt;CLAY&lt;/span&gt;(&lt;span class="pl-en"&gt;CLAY_ID&lt;/span&gt;(&lt;span class="pl-s"&gt;"SyntaxPageRightImageInner"&lt;/span&gt;), &lt;span class="pl-en"&gt;CLAY_LAYOUT&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sizing&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; { &lt;span class="pl-en"&gt;CLAY_SIZING_GROW&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;max&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;568&lt;/span&gt; }) } }), &lt;span class="pl-c1"&gt;CLAY_IMAGE&lt;/span&gt;({ .&lt;span class="pl-s1"&gt;sourceDimensions&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {&lt;span class="pl-c1"&gt;1136&lt;/span&gt;, &lt;span class="pl-c1"&gt;1194&lt;/span&gt;}, .&lt;span class="pl-s1"&gt;sourceURL&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;CLAY_STRING&lt;/span&gt;(&lt;span class="pl-s"&gt;"/clay/images/declarative.png"&lt;/span&gt;) })) {}
      }
    }
  }
}&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I'm not ready to ditch HTML and CSS for writing my web pages in C compiled to WebAssembly just yet, but as an exercise in understanding layout engines (and a potential tool for building non-web interfaces in the future) this is a really interesting project to dig into.&lt;/p&gt;
&lt;p&gt;To clarify here: I don't think the web layout / WebAssembly thing is the key idea behind Clay at all - I think it's a neat demo of the library, but it's not what Clay is &lt;em&gt;for&lt;/em&gt;. It's certainly an interesting way to provide a demo of a layout library!&lt;/p&gt;
&lt;p&gt;Nic &lt;a href="https://bsky.app/profile/nicbarker.com/post/3ldu44rxyx22h"&gt;confirms&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You totally nailed it, the fact that you can compile to wasm and run in HTML stemmed entirely from a “wouldn’t it be cool if…” It was designed for my C projects first and foremost!&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=42463123"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/accessibility"&gt;accessibility&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="c"/><category term="webassembly"/><category term="html"/><category term="accessibility"/></entry><entry><title>You can use text-wrap: balance; on icons</title><link href="https://simonwillison.net/2024/Oct/20/you-can-use-text-wrap-balance-on-icons/#atom-tag" rel="alternate"/><published>2024-10-20T13:23:16+00:00</published><updated>2024-10-20T13:23:16+00:00</updated><id>https://simonwillison.net/2024/Oct/20/you-can-use-text-wrap-balance-on-icons/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://shkspr.mobi/blog/2024/10/you-can-use-text-wrap-balance-on-icons/"&gt;You can use text-wrap: balance; on icons&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat CSS experiment from Terence Eden: the new &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap#balance"&gt;text-wrap: balance&lt;/a&gt; CSS property is intended to help make text like headlines display without ugly wrapped single orphan words, but Terence points out it can be used for icons too:&lt;/p&gt;
&lt;p&gt;&lt;img alt="A row of icons, without text-wrap balances just one is wrapped on the second line. With the propert they are split into two lines with equal numbers of icons." src="https://static.simonwillison.net/static/2024/icons-text-wrap-balance.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;This inspired me to investigate if the same technique could work for text based navigation elements. I &lt;a href="https://gist.github.com/simonw/53648554917862676ccd12dcf5cc9cab"&gt;used Claude&lt;/a&gt; to build &lt;a href="https://tools.simonwillison.net/text-wrap-balance-nav"&gt;this interactive prototype&lt;/a&gt; of a navigation bar that uses &lt;code&gt;text-wrap: balance&lt;/code&gt; against a list of &lt;code&gt;display: inline&lt;/code&gt; menu list items. It seems to work well!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo. A navigation menu with 13 items - things like Home and About and Services and a products. These are wrapped on four lines with 4, 4, 4 and then 1 item. Selecting the enable text-wrap: balances checkbox changes that to 3, 4, 3, 3 - a slider also allows the number of visible items to be changed to see the effect that has" src="https://static.simonwillison.net/static/2024/text-wrap-balance.gif" /&gt;&lt;/p&gt;
&lt;p&gt;My first attempt used &lt;code&gt;display: inline-block&lt;/code&gt; which worked in Safari but failed in Firefox.&lt;/p&gt;
&lt;p&gt;Notable limitation from &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap#balance"&gt;that MDN article&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Because counting characters and balancing them across multiple lines is computationally expensive, this value is only supported for blocks of text spanning a limited number of lines (six or less for Chromium and ten or less for Firefox)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So it's fine for these navigation concepts but isn't something you can use for body text.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/terence-eden"&gt;terence-eden&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prototyping"&gt;prototyping&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="claude-artifacts"/><category term="anthropic"/><category term="ai-assisted-programming"/><category term="claude"/><category term="terence-eden"/><category term="prompt-to-app"/><category term="prototyping"/></entry><entry><title>HTML for People</title><link href="https://simonwillison.net/2024/Oct/11/html-for-people/#atom-tag" rel="alternate"/><published>2024-10-11T01:51:43+00:00</published><updated>2024-10-11T01:51:43+00:00</updated><id>https://simonwillison.net/2024/Oct/11/html-for-people/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://htmlforpeople.com/"&gt;HTML for People&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Blake Watson's brand new HTML tutorial, presented as a free online book (CC BY-NC-SA 4.0, &lt;a href="https://github.com/blakewatson/htmlforpeople"&gt;on GitHub&lt;/a&gt;). This seems very modern and well thought-out to me. It focuses exclusively on HTML, skipping JavaScript entirely and teaching with &lt;a href="https://simplecss.org/"&gt;Simple.css&lt;/a&gt; to avoid needing to dig into CSS while still producing sites that are pleasing to look at. It even touches on Web Components (described as &lt;a href="https://htmlforpeople.com/adding-a-fun-page/#custom-html-tags"&gt;Custom HTML tags&lt;/a&gt;) towards the end.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=41801334"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-components"&gt;web-components&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="html"/><category term="web-components"/></entry><entry><title>Calling LLMs from client-side JavaScript, converting PDFs to HTML + weeknotes</title><link href="https://simonwillison.net/2024/Sep/6/weeknotes/#atom-tag" rel="alternate"/><published>2024-09-06T02:28:38+00:00</published><updated>2024-09-06T02:28:38+00:00</updated><id>https://simonwillison.net/2024/Sep/6/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;I've been having a bunch of fun taking advantage of CORS-enabled LLM APIs to build client-side JavaScript applications that access LLMs directly. I also span up a new Datasette plugin for advanced permission management.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#llms-from-client-side-javascript"&gt;LLMs from client-side JavaScript&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#converting-pdfs-to-html-and-markdown"&gt;Converting PDFs to HTML and Markdown&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#adding-some-class-to-datasette-forms"&gt;Adding some class to Datasette forms&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#on-the-blog"&gt;On the blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#releases"&gt;Releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Sep/6/weeknotes/#tils"&gt;TILs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="llms-from-client-side-javascript"&gt;LLMs from client-side JavaScript&lt;/h4&gt;
&lt;p&gt;Anthropic &lt;a href="https://simonwillison.net/2024/Aug/23/anthropic-dangerous-direct-browser-access/"&gt;recently added CORS support&lt;/a&gt; to their Claude APIs. It's a little hard to use - you have to add &lt;code&gt;anthropic-dangerous-direct-browser-access: true&lt;/code&gt; to your request headers to enable it - but once you know the trick you can start building web applications that talk to Anthropic's LLMs directly, without any additional server-side code.&lt;/p&gt;
&lt;p&gt;I later found out that both OpenAI and Google Gemini have this capability too, without needing the special header.&lt;/p&gt;
&lt;p&gt;The problem with this approach is security: it's very important not to embed an API key attached to your billing account in client-side HTML and JavaScript for anyone to see!&lt;/p&gt;
&lt;p&gt;For my purposes though that doesn't matter. I've been building tools which &lt;code&gt;prompt()&lt;/code&gt; a user for their own API key (sadly restricting their usage to the tiny portion of people who both understand API keys and have created API accounts with one of the big providers) - then I stash that key in &lt;code&gt;localStorage&lt;/code&gt; and start using it to make requests.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; repository is home to a growing collection of pure HTML+JavaScript tools, hosted at &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; using GitHub Pages. I love not having to even think about hosting server-side code for these tools.&lt;/p&gt;
&lt;p&gt;I've published three tools there that talk to LLMs directly so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tools.simonwillison.net/haiku"&gt;haiku&lt;/a&gt; is a fun demo that requests access to the user's camera and then writes a Haiku about what it sees. It uses Anthropic's Claude 3 Haiku model for this - the whole project is one terrible pun. &lt;a href="https://github.com/simonw/tools/blob/main/haiku.html"&gt;Haiku source code here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tools.simonwillison.net/gemini-bbox"&gt;gemini-bbox&lt;/a&gt; uses the Gemini 1.5 Pro (or Flash) API to prompt those models to return bounding boxes for objects in an image, then renders those bounding boxes. Gemini Pro is the only of the vision LLMs that I've tried that has reliable support for bounding boxes. I wrote about this in &lt;a href="https://simonwillison.net/2024/Aug/26/gemini-bounding-box-visualization/"&gt;Building a tool showing how Gemini Pro can return bounding boxes for objects in images&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tools.simonwillison.net/gemini-chat"&gt;Gemini Chat App&lt;/a&gt; is a more traditional LLM chat interface that again talks to Gemini models (including the new super-speedy &lt;code&gt;gemini-1.5-flash-8b-exp-0827&lt;/code&gt;). I built this partly to try out those new models and partly to experiment with implementing a streaming chat interface agaist the Gemini API directly in a browser. I wrote more about how that works &lt;a href="https://simonwillison.net/2024/Aug/27/gemini-chat-app/"&gt;in this post&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's that Gemini Bounding Box visualization tool:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/goats-bbox-fixed.jpg" alt="Gemini API Image Bounding Box Visualization - browse for file goats.jpeg, prompt is Return bounding boxes as JSON arrays [ymin, xmin, ymax, xmax] - there follows output coordinates and then a red and a green box around the goats in a photo, with grid lines showing the coordinates from 0-1000 on both axes" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;All three of these tools made heavy use of AI-assisted development: Claude 3.5 Sonnet wrote almost every line of the last two, and the Haiku one was put together a few months ago using Claude 3 Opus.&lt;/p&gt;
&lt;p&gt;My personal style of HTML and JavaScript apps turns out to be highly compatible with LLMs: I like using vanilla HTML and JavaScript and keeping everything in the same file, which makes it easy to paste the entire thing into the model and ask it to make some changes for me. This approach also works really well with &lt;a href="https://simonwillison.net/tags/claude-artifacts/"&gt;Claude Artifacts&lt;/a&gt;, though I have to tell it "no React" to make sure I get an artifact I can hack on without needing to configure a React build step.&lt;/p&gt;
&lt;h4 id="converting-pdfs-to-html-and-markdown"&gt;Converting PDFs to HTML and Markdown&lt;/h4&gt;
&lt;p&gt;I have a long standing vendetta against PDFs for sharing information. They're painful to read on a mobile phone, they have poor accessibility, and even things like copying and pasting text from them can be a pain.&lt;/p&gt;
&lt;p&gt;Complaining without doing something about it isn't really my style. Twice in the past few weeks I've taken matters into my own hands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Research released &lt;a href="https://research.google/pubs/sql-has-problems-we-can-fix-them-pipe-syntax-in-sql/"&gt;a PDF paper&lt;/a&gt; describing their new pipe syntax for SQL. I ran it through Gemini 1.5 Pro to convert it to HTML (&lt;a href="https://simonwillison.net/2024/Aug/24/pipe-syntax-in-sql/"&gt;prompts here&lt;/a&gt;) and &lt;a href="https://static.simonwillison.net/static/2024/Pipe-Syntax-In-SQL.html"&gt;got this&lt;/a&gt; - a pretty great initial result for the first prompt I tried!&lt;/li&gt;
&lt;li&gt;Nous Research released &lt;a href="https://github.com/NousResearch/DisTrO/blob/main/A_Preliminary_Report_on_DisTrO.pdf"&gt;a preliminary report PDF&lt;/a&gt; about their DisTro technology for distributed training of LLMs over low-bandwidth connections. I &lt;a href="https://simonwillison.net/2024/Aug/27/distro/"&gt;ran a prompt&lt;/a&gt; to use Gemini 1.5 Pro to convert that to &lt;a href="https://gist.github.com/simonw/46a33d66e069efe5c10b63625fdabb4e"&gt;this Markdown version&lt;/a&gt;, which even handled tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Within six hours of posting it my Pipe Syntax in SQL conversion was ranked third on Google for the title of the paper, at which point I set it to &lt;code&gt;&amp;lt;meta name="robots" content="noindex&amp;gt;&lt;/code&gt; to try and keep the unverified clone out of search. Yet more evidence that HTML is better than PDF!&lt;/p&gt;
&lt;p&gt;I've spent less than a total of ten minutes on using Gemini to convert PDFs in this way and the results have been very impressive. If I were to spend more time on this I'd target figures: I have a hunch that getting Gemini to return bounding boxes for figures on the PDF pages could be the key here, since then each figure could be automatically extracted as an image.&lt;/p&gt;
&lt;p&gt;I bet you could build that whole thing as a client-side app against the Gemini Pro API, too...&lt;/p&gt;
&lt;h4 id="adding-some-class-to-datasette-forms"&gt;Adding some class to Datasette forms&lt;/h4&gt;
&lt;p&gt;I've  been working on a new Datasette plugin for permissions management, &lt;a href="https://github.com/datasette/datasette-acl"&gt;datasette-acl&lt;/a&gt;, which I'll write about separately soon.&lt;/p&gt;
&lt;p&gt;I wanted to integrate &lt;a href="https://github.com/Choices-js/Choices"&gt;Choices.js&lt;/a&gt; with it, to provide a nicer interface for adding permissions to a user or group.&lt;/p&gt;
&lt;p&gt;My first attempt at integrating Choices ended up looking like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-acl-choices-bug.jpg" alt="The choices elements have big upgly blank boxes displayed where the remove icon should be. The Firefox DevTools console is open revealing CSS properties set on form button type=button, explaining the visual glitches" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The weird visual glitches are caused by Datasette's core CSS, which included &lt;a href="https://github.com/simonw/datasette/blob/92c4d41ca605e0837a2711ee52fde9cf1eea74d0/datasette/static/app.css#L553-L564"&gt;the following rule&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-css"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;form&lt;/span&gt; &lt;span class="pl-ent"&gt;input&lt;/span&gt;[&lt;span class="pl-c1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;submit&lt;/span&gt;]&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-ent"&gt;form&lt;/span&gt; &lt;span class="pl-ent"&gt;button&lt;/span&gt;[&lt;span class="pl-c1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;button&lt;/span&gt;] {
    &lt;span class="pl-c1"&gt;font-weight&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;400&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;cursor&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; pointer;
    &lt;span class="pl-c1"&gt;text-align&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; center;
    &lt;span class="pl-c1"&gt;vertical-align&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; middle;
    &lt;span class="pl-c1"&gt;border-width&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;span class="pl-smi"&gt;px&lt;/span&gt;&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;border-style&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; solid;
    &lt;span class="pl-c1"&gt;padding&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;.5&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;0.8&lt;span class="pl-smi"&gt;em&lt;/span&gt;&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;font-size&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0.9&lt;span class="pl-smi"&gt;rem&lt;/span&gt;&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;line-height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;;
    &lt;span class="pl-c1"&gt;border-radius&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;.25&lt;span class="pl-smi"&gt;rem&lt;/span&gt;&lt;/span&gt;;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These style rules apply to &lt;em&gt;any&lt;/em&gt; submit button or button-button that occurs inside a form!&lt;/p&gt;
&lt;p&gt;I'm glad I caught this before Datasette 1.0. I've now &lt;a href="https://github.com/simonw/datasette/issues/2415"&gt;started the process of fixing that&lt;/a&gt;, by ensuring these rules only apply to elements with &lt;code&gt;class="core"&lt;/code&gt; (or that class on a wrapping element). This ensures plugins can style these elements without being caught out by Datasette's defaults.&lt;/p&gt;
&lt;p&gt;The problem is... there are a whole bunch of existing plugins that currently rely on that behaviour. I have &lt;a href="https://github.com/simonw/datasette/issues/2417"&gt;a tricking issue&lt;/a&gt; about that, which identified 28 plugins that need updating. I've worked my way through 8 of those so far, hence the flurry of releases listed at the bottom of this post.&lt;/p&gt;
&lt;p&gt;This is also an excuse to revisit a bunch of older plugins, some of which had partially complete features that I've been finishing up.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-write"&gt;datasette-write&lt;/a&gt; for example now has &lt;a href="https://github.com/simonw/datasette-write/issues/10"&gt;a neat row action menu item&lt;/a&gt; for updating a selected row using a pre-canned UPDATE query. Here's an animated demo of my first prototype of that feature:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-write-row.gif" alt="Animated demo - on the row page for a release I click row actions and select Update using SQL, which navigates to a page with a big UPDATE SQL query and a form showing all of the existing values." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="on-the-blog"&gt;On the blog&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;anthropic&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/23/anthropic-dangerous-direct-browser-access"&gt;Claude's API now supports CORS requests, enabling client-side applications&lt;/a&gt; - 2024-08-23&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/23/explain-acls"&gt;Explain ACLs by showing me a SQLite table schema for implementing them&lt;/a&gt; - 2024-08-23&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/24/oauth-llms"&gt;Musing about OAuth and LLMs on Mastodon&lt;/a&gt; - 2024-08-24&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/gemini-bounding-box-visualization"&gt;Building a tool showing how Gemini Pro can return bounding boxes for objects in images&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/long-context-prompting-tips"&gt;Long context prompting tips&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/anthropic-system-prompts"&gt;Anthropic Release Notes: System Prompts&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/alex-albert"&gt;Alex Albert: We've read and heard that you'd appreciate more t...&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/27/gemini-chat-app"&gt;Gemini Chat App&lt;/a&gt; - 2024-08-27&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/28/system-prompt-for-townie"&gt;System prompt for val.town/townie&lt;/a&gt; - 2024-08-28&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/28/how-anthropic-built-artifacts"&gt;How Anthropic built Artifacts&lt;/a&gt; - 2024-08-28&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/30/anthropic-prompt-engineering-interactive-tutorial"&gt;Anthropic's Prompt Engineering Interactive Tutorial&lt;/a&gt; - 2024-08-30&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/30/llm-claude-3"&gt;llm-claude-3 0.4.1&lt;/a&gt; - 2024-08-30&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ai-assisted-programming&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/24/andy-jassy-amazon-ceo"&gt;Andy Jassy, Amazon CEO: [...] here’s what we found when we integrated [Am...&lt;/a&gt; - 2024-08-24&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/ai-powered-git-commit-function"&gt;AI-powered Git Commit Function&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/30/openai-file-search"&gt;OpenAI: Improve file search result relevance with chunk ranking&lt;/a&gt; - 2024-08-30&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/31/forrest-brazeal"&gt;Forrest Brazeal: I think that AI has killed, or is about to kill, ...&lt;/a&gt; - 2024-08-31&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;gemini&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/24/pipe-syntax-in-sql"&gt;SQL Has Problems. We Can Fix Them: Pipe Syntax In SQL&lt;/a&gt; - 2024-08-24&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/27/distro"&gt;NousResearch/DisTrO&lt;/a&gt; - 2024-08-27&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;python&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/1/uvtrick"&gt;uvtrick&lt;/a&gt; - 2024-09-01&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/2/anatomy-of-a-textual-user-interface"&gt;Anatomy of a Textual User Interface&lt;/a&gt; - 2024-09-02&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/2/why-i-still-use-python-virtual-environments-in-docker"&gt;Why I Still Use Python Virtual Environments in Docker&lt;/a&gt; - 2024-09-02&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/3/python-developers-survey-2023"&gt;Python Developers Survey 2023 Results&lt;/a&gt; - 2024-09-03&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;security&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/23/microsoft-copilot-data-governance"&gt;Top companies ground Microsoft Copilot over data governance concerns&lt;/a&gt; - 2024-08-23&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/26/frederik-braun"&gt;Frederik Braun: In 2021 we [the Mozilla engineering team] found “...&lt;/a&gt; - 2024-08-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/5/oauth-from-first-principles"&gt;OAuth from First Principles&lt;/a&gt; - 2024-09-05&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;projects&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/25/covidsewage-alt-text"&gt;My @covidsewage bot now includes useful alt text&lt;/a&gt; - 2024-08-25&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;armin-ronacher&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/27/minijinja"&gt;MiniJinja: Learnings from Building a Template Engine in Rust&lt;/a&gt; - 2024-08-27&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ethics&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/27/john-gruber"&gt;John Gruber: Everyone alive today has grown up in a world wher...&lt;/a&gt; - 2024-08-27&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;open-source&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/27/open-source-ai"&gt;Debate over “open source AI” term brings new push to formalize definition&lt;/a&gt; - 2024-08-27&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/29/elasticsearch-is-open-source-again"&gt;Elasticsearch is open source, again&lt;/a&gt; - 2024-08-29&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;performance&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/28/cerebras-inference"&gt;Cerebras Inference: AI at Instant Speed&lt;/a&gt; - 2024-08-28&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;sqlite&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/28/d-richard-hipp"&gt;D. Richard Hipp: My goal is to keep SQLite relevant and viable thr...&lt;/a&gt; - 2024-08-28&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;aws&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/30/leader-election-with-s3-conditional-writes"&gt;Leader Election With S3 Conditional Writes&lt;/a&gt; - 2024-08-30&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;javascript&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/31/andreas-giammarchi"&gt;Andreas Giammarchi: whenever you do this: `el.innerHTML += HTML`  ...&lt;/a&gt; - 2024-08-31&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;openai&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/31/openai-says-chatgpt-usage-has-doubled-since-last-year"&gt;OpenAI says ChatGPT usage has doubled since last year&lt;/a&gt; - 2024-08-31&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;art&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Aug/31/ted-chiang"&gt;Ted Chiang: Art is notoriously hard to define, and so are the...&lt;/a&gt; - 2024-08-31&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;llm&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/3/anjor"&gt;anjor: `history | tail -n 2000 | llm -s "Write aliases f...&lt;/a&gt; - 2024-09-03&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;vision-llms&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/4/qwen2-vl"&gt;Qwen2-VL: To See the World More Clearly&lt;/a&gt; - 2024-09-04&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="releases"&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-import/releases/tag/0.1a5"&gt;datasette-import 0.1a5&lt;/a&gt;&lt;/strong&gt; - 2024-09-04&lt;br /&gt;Tools for importing data into Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-search-all/releases/tag/1.1.3"&gt;datasette-search-all 1.1.3&lt;/a&gt;&lt;/strong&gt; - 2024-09-04&lt;br /&gt;Datasette plugin for searching all searchable tables at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-write/releases/tag/0.4"&gt;datasette-write 0.4&lt;/a&gt;&lt;/strong&gt; - 2024-09-04&lt;br /&gt;Datasette plugin providing a UI for executing SQL writes against the database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-debug-events/releases/tag/0.1a0"&gt;datasette-debug-events 0.1a0&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Print Datasette events to standard error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-auth-passwords/releases/tag/1.1.1"&gt;datasette-auth-passwords 1.1.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Datasette plugin for authentication using passwords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments/releases/tag/0.4.3"&gt;datasette-enrichments 0.4.3&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Tools for running enrichments against data stored in Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-configure-fts/releases/tag/1.1.4"&gt;datasette-configure-fts 1.1.4&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Datasette plugin for enabling full-text search against selected table columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-auth-tokens/releases/tag/0.4a10"&gt;datasette-auth-tokens 0.4a10&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Datasette plugin for authenticating access using API tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-edit-schema/releases/tag/0.8a3"&gt;datasette-edit-schema 0.8a3&lt;/a&gt;&lt;/strong&gt; - 2024-09-03&lt;br /&gt;Datasette plugin for modifying table schemas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-pins/releases/tag/0.1a4"&gt;datasette-pins 0.1a4&lt;/a&gt;&lt;/strong&gt; - 2024-09-01&lt;br /&gt;Pin databases, tables, and other items to the Datasette homepage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-acl/releases/tag/0.4a2"&gt;datasette-acl 0.4a2&lt;/a&gt;&lt;/strong&gt; - 2024-09-01&lt;br /&gt;Advanced permission management for Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-claude-3/releases/tag/0.4.1"&gt;llm-claude-3 0.4.1&lt;/a&gt;&lt;/strong&gt; - 2024-08-30&lt;br /&gt;LLM plugin for interacting with the Claude 3 family of models&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="tils"&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/playwright/testing-tables"&gt;Testing HTML tables with Playwright Python&lt;/a&gt; - 2024-09-04&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/pytest/namedtuple-parameterized-tests"&gt;Using namedtuple for pytest parameterized tests&lt;/a&gt; - 2024-08-31&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pdf"&gt;pdf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="css"/><category term="claude-3-5-sonnet"/><category term="gemini"/><category term="anthropic"/><category term="claude"/><category term="cors"/><category term="ai"/><category term="llms"/><category term="pdf"/><category term="javascript"/><category term="datasette"/><category term="projects"/><category term="generative-ai"/><category term="weeknotes"/></entry><entry><title>Button Stealer</title><link href="https://simonwillison.net/2024/Jul/25/button-stealer/#atom-tag" rel="alternate"/><published>2024-07-25T19:40:08+00:00</published><updated>2024-07-25T19:40:08+00:00</updated><id>https://simonwillison.net/2024/Jul/25/button-stealer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://anatolyzenkov.com/stolen-buttons/button-stealer"&gt;Button Stealer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Really fun Chrome extension by Anatoly Zenkov: it scans every web page you visit for things that look like buttons and stashes a copy of them, then provides a page where you can see all of the buttons you have collected. Here's &lt;a href="https://anatolyzenkov.com/stolen-buttons"&gt;Anatoly's collection&lt;/a&gt;, and here are a few that I've picked up trying it out myself:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot showing some buttons I have collected, each with their visual appearance maintained" src="https://static.simonwillison.net/static/2024/stolen-buttons.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;The extension source code is &lt;a href="https://github.com/anatolyzenkov/button-stealer"&gt;on GitHub&lt;/a&gt;. It identifies potential buttons by looping through every &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element and &lt;a href="https://github.com/anatolyzenkov/button-stealer/blob/cfe43b6247e1b9f7d4414fd2a9b122c2d1a40840/scripts/button-stealer.js#L264-L298"&gt;applying some heuristics&lt;/a&gt; like checking the width/height ratio, then &lt;a href="https://github.com/anatolyzenkov/button-stealer/blob/cfe43b6247e1b9f7d4414fd2a9b122c2d1a40840/scripts/button-stealer.js#L93-L140"&gt;clones a subset of the CSS&lt;/a&gt; from &lt;code&gt;window.getComputedStyle()&lt;/code&gt; and stores that in the &lt;code&gt;style=&lt;/code&gt; attribute.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://waxy.org/2024/07/button-stealer/"&gt;Andy Baio&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/extensions"&gt;extensions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="chrome"/><category term="extensions"/><category term="javascript"/></entry><entry><title>So you think you know box shadows?</title><link href="https://simonwillison.net/2024/Jul/21/so-you-think-you-know-box-shadows/#atom-tag" rel="alternate"/><published>2024-07-21T16:23:39+00:00</published><updated>2024-07-21T16:23:39+00:00</updated><id>https://simonwillison.net/2024/Jul/21/so-you-think-you-know-box-shadows/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dgerrells.com/blog/how-not-to-use-box-shadows"&gt;So you think you know box shadows?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
David Gerrells dives &lt;em&gt;deep&lt;/em&gt; into CSS box shadows. How deep? Implementing a full ray tracer with them deep.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=41024664"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="javascript"/></entry><entry><title>Box shadow CSS generator</title><link href="https://simonwillison.net/2024/Jul/8/box-shadow-css-generator/#atom-tag" rel="alternate"/><published>2024-07-08T19:30:41+00:00</published><updated>2024-07-08T19:30:41+00:00</updated><id>https://simonwillison.net/2024/Jul/8/box-shadow-css-generator/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/box-shadow"&gt;Box shadow CSS generator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another example of a tiny personal tool I built using Claude 3.5 Sonnet and artifacts. In this case my prompt was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CSS for a slight box shadow, build me a tool that helps me twiddle settings and preview them and copy and paste out the CSS&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I changed my mind half way through typing the prompt and asked it for a custom tool, and it built me this!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://tools.simonwillison.net/box-shadow"&gt;&lt;img src="https://static.simonwillison.net/static/2024/box-shadow.jpg" alt="Box shadow CSS generator. Shows a preview, then provides sliders to set Horizontal Offset, Vertical Offset, Blur Radius,  Spread Radius,  Color and Opacity - plus the generated CSS and a Copy to Clipboard button" width="400" class="blogmark-image"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/ffbf7d7abbf56a126c89e7d62442696a"&gt;the full transcript&lt;/a&gt; - in a follow-up prompt I asked for help deploying it and it rewrote the tool to use &lt;code&gt;&amp;lt;script type="text/babel"&amp;gt;&lt;/code&gt; and the &lt;a href="https://babeljs.io/docs/babel-standalone"&gt;babel-standalone&lt;/a&gt; library to add React JSX support directly in the browser - a bit of a hefty dependency (387KB compressed / 2.79MB total) but I think acceptable for this kind of one-off tool.&lt;/p&gt;
&lt;p&gt;Being able to knock out tiny custom tools like this on a whim is a really interesting new capability. It's also a lot of fun!

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1810335524017877240"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="anthropic"/><category term="claude"/><category term="generative-ai"/><category term="projects"/><category term="ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude-artifacts"/><category term="claude-3-5-sonnet"/><category term="prompt-to-app"/></entry><entry><title>What You Need to Know about Modern CSS (Spring 2024 Edition)</title><link href="https://simonwillison.net/2024/May/5/modern-css/#atom-tag" rel="alternate"/><published>2024-05-05T14:08:01+00:00</published><updated>2024-05-05T14:08:01+00:00</updated><id>https://simonwillison.net/2024/May/5/modern-css/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://frontendmasters.com/blog/what-you-need-to-know-about-modern-css-spring-2024-edition/"&gt;What You Need to Know about Modern CSS (Spring 2024 Edition)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Useful guide to the many new CSS features that have become widely enough supported to start using as-of May 2024. Time to learn container queries!&lt;/p&gt;

&lt;p&gt;View transitions are still mostly limited to Chrome—I can’t wait for those to land in Firefox and Safari.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/4wvga3/what_you_need_know_about_modern_css"&gt;Lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/view-transitions"&gt;view-transitions&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="view-transitions"/></entry><entry><title>Printing music with CSS Grid</title><link href="https://simonwillison.net/2024/May/2/printing-music-with-css-grid/#atom-tag" rel="alternate"/><published>2024-05-02T14:28:33+00:00</published><updated>2024-05-02T14:28:33+00:00</updated><id>https://simonwillison.net/2024/May/2/printing-music-with-css-grid/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://cruncher.ch/blog/printing-music-with-css-grid/"&gt;Printing music with CSS Grid&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stephen Bond demonstrates some ingenious tricks for creating surprisingly usable sheet music notation using clever application of CSS grids.&lt;/p&gt;
&lt;p&gt;It uses rules like &lt;code&gt;.stave &amp;gt; [data-duration="0.75"] { grid-column-end: span 18; }&lt;/code&gt; to turn &lt;code&gt;data-&lt;/code&gt; attributes for musical properties into positions on the rendered stave.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=40216057"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/music"&gt;music&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="music"/></entry><entry><title>Kobold letters</title><link href="https://simonwillison.net/2024/Apr/4/kobold-letters/#atom-tag" rel="alternate"/><published>2024-04-04T12:43:41+00:00</published><updated>2024-04-04T12:43:41+00:00</updated><id>https://simonwillison.net/2024/Apr/4/kobold-letters/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lutrasecurity.com/en/articles/kobold-letters/"&gt;Kobold letters&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Konstantin Weddige explains a sophisticated HTML email phishing vector he calls Kobold emails.&lt;/p&gt;

&lt;p&gt;When you forward a message, most HTML email clients will indent the forward by nesting it inside another element.&lt;/p&gt;

&lt;p&gt;This means CSS rules within the email can be used to cause an element that was invisible in the original email to become visible when it is forwarded—allowing tricks like a forwarded innocuous email from your boss adding instructions for wiring money from the company bank account.&lt;/p&gt;

&lt;p&gt;Gmail strips style blocks before forwarding—which it turns out isn’t protection against this, because you can put a style block in the original email to hide the attack text which will then be stripped for you when the email is forwarded.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=39928558"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/email"&gt;email&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="security"/><category term="email"/></entry><entry><title>The Dropflow Playground</title><link href="https://simonwillison.net/2024/Mar/22/the-dropflow-playground/#atom-tag" rel="alternate"/><published>2024-03-22T01:33:44+00:00</published><updated>2024-03-22T01:33:44+00:00</updated><id>https://simonwillison.net/2024/Mar/22/the-dropflow-playground/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://chearon.github.io/dropflow/"&gt;The Dropflow Playground&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Dropflow is a “CSS layout engine” written in TypeScript and taking advantage of the HarfBuzz text shaping engine (used by Chrome, Android, Firefox and more) compiled to WebAssembly to implement glyph layout.&lt;/p&gt;

&lt;p&gt;This linked demo is fascinating: on the left hand side you can edit HTML with inline styles, and the right hand side then updates live to show that content rendered by Dropflow in a canvas element.&lt;/p&gt;

&lt;p&gt;Why would you want this? It lets you generate images and PDFs with excellent performance using your existing knowledge HTML and CSS. It’s also just really cool!

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/chearon/dropflow"&gt;chearon/dropflow&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/typescript"&gt;typescript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="typescript"/><category term="webassembly"/><category term="javascript"/></entry><entry><title>Upside down table trick with CSS</title><link href="https://simonwillison.net/2024/Feb/24/upside-down-table-trick-with-css/#atom-tag" rel="alternate"/><published>2024-02-24T21:00:23+00:00</published><updated>2024-02-24T21:00:23+00:00</updated><id>https://simonwillison.net/2024/Feb/24/upside-down-table-trick-with-css/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://codepen.io/simonwillison/pen/GRebPKr"&gt;Upside down table trick with CSS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I was complaining how hard it is to build a horizontally scrollable table with a scrollbar at the top rather than the bottom and RGBCube on Lobste.rs suggested rotating the container 180 degrees and then the table contents and headers 180 back again... and it totally works! Demo in this CodePen.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/dghv8d/please_make_your_table_headings_sticky#c_iztvit"&gt;Lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/></entry><entry><title>How To Center a Div</title><link href="https://simonwillison.net/2024/Feb/13/how-to-center-a-div/#atom-tag" rel="alternate"/><published>2024-02-13T19:51:42+00:00</published><updated>2024-02-13T19:51:42+00:00</updated><id>https://simonwillison.net/2024/Feb/13/how-to-center-a-div/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.joshwcomeau.com/css/center-a-div/"&gt;How To Center a Div&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Josh Comeau: “I think that my best blog posts are accessible to beginners while still having some gold nuggets for more experienced devs, and I think I’ve nailed that here. Even if you have years of CSS experience, I bet you’ll learn something new.”&lt;/p&gt;

&lt;p&gt;Lots of interactive demos in this.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/joshwcomeau/status/1757475065992474680"&gt;@joshwcomeau&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/josh-comeau"&gt;josh-comeau&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="josh-comeau"/></entry><entry><title>An Interactive Guide to CSS Grid</title><link href="https://simonwillison.net/2023/Nov/21/an-interactive-guide-to-css-grid/#atom-tag" rel="alternate"/><published>2023-11-21T16:25:55+00:00</published><updated>2023-11-21T16:25:55+00:00</updated><id>https://simonwillison.net/2023/Nov/21/an-interactive-guide-to-css-grid/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.joshwcomeau.com/css/interactive-guide-to-grid/"&gt;An Interactive Guide to CSS Grid&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Josh Comeau’s extremely clear guide to CSS grid, with interactive examples for all of the core properties.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/joshwcomeau/status/1726989436758675905"&gt;@joshwcomeau&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/josh-comeau"&gt;josh-comeau&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="josh-comeau"/></entry><entry><title>The anatomy of visually-hidden</title><link href="https://simonwillison.net/2023/Feb/11/the-anatomy-of-visually-hidden/#atom-tag" rel="alternate"/><published>2023-02-11T00:37:28+00:00</published><updated>2023-02-11T00:37:28+00:00</updated><id>https://simonwillison.net/2023/Feb/11/the-anatomy-of-visually-hidden/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.tpgi.com/the-anatomy-of-visually-hidden/"&gt;The anatomy of visually-hidden&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
James Edwards provides a detailed breakdown of the current recommended CSS for hiding content while keeping it available for assistive technologies in the browser accessibility and render trees. Lots of accumulated tricks and screen reader special cases in this.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/bendmyers/status/1624148295609114665"&gt;Ben Myers&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/screen-readers"&gt;screen-readers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/accessibility"&gt;accessibility&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="screen-readers"/><category term="accessibility"/></entry></feed>