<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: html</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/html.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-05-23T20:24:48+00:00</updated><author><name>Simon Willison</name></author><entry><title>On the &lt;dl&gt;</title><link href="https://simonwillison.net/2026/May/23/on-the-dl/#atom-tag" rel="alternate"/><published>2026-05-23T20:24:48+00:00</published><updated>2026-05-23T20:24:48+00:00</updated><id>https://simonwillison.net/2026/May/23/on-the-dl/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://benmyers.dev/blog/on-the-dl/"&gt;On the &amp;lt;dl&amp;gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I learned a few new-to-me things about the &lt;code&gt;&amp;lt;dl&amp;gt;&lt;/code&gt; element from this article by Ben Meyer:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;dt&amp;gt;&lt;/code&gt; can be followed by &lt;em&gt;multiple&lt;/em&gt; &lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can optionally group the &lt;code&gt;&amp;lt;dt&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt; elements in a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; for styling - but only a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can label them using ARIA.&lt;/li&gt;
&lt;li&gt;They've been called "description lists", not "definition lists", since &lt;a href="https://www.w3.org/TR/2008/WD-html5-20080122/#the-dl"&gt;an HTML5 draft in 2008&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So this is valid:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;h2&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;credits&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Credits&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;h2&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;dl&lt;/span&gt; &lt;span class="pl-c1"&gt;aria-labelledby&lt;/span&gt;="&lt;span class="pl-s"&gt;credits&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;dt&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Author&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;dt&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;dd&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Jeffrey Zeldman&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;dd&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;dd&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Ethan Marcotte&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;dd&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;dl&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Here's a useful note from Adrian Roselli on &lt;a href="https://adrianroselli.com/2025/01/updated-brief-note-on-description-list-support.html"&gt;screen reader support for description lists&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=48247325"&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/screen-readers"&gt;screen-readers&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="screen-readers"/><category term="web-standards"/></entry><entry><title>Using Claude Code: The Unreasonable Effectiveness of HTML</title><link href="https://simonwillison.net/2026/May/8/unreasonable-effectiveness-of-html/#atom-tag" rel="alternate"/><published>2026-05-08T21:00:11+00:00</published><updated>2026-05-08T21:00:11+00:00</updated><id>https://simonwillison.net/2026/May/8/unreasonable-effectiveness-of-html/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://twitter.com/trq212/status/2052809885763747935"&gt;Using Claude Code: The Unreasonable Effectiveness of HTML&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Thought-provoking piece by Thariq Shihipar (on the Claude Code team at Anthropic) advocating for HTML over Markdown as an output format to request from Claude.&lt;/p&gt;
&lt;p&gt;The article is crammed with interesting examples (collected on &lt;a href="https://thariqs.github.io/html-effectiveness/"&gt;this site&lt;/a&gt;) and prompt suggestions like this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Help me review this PR by creating an HTML artifact that describes it. I'm not very familiar with the streaming/backpressure logic so focus on that. Render the actual diff with inline margin annotations, color-code findings by severity and whatever else might be needed to convey the concept well.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've been defaulting to asking for most things in Markdown since the GPT-4 days, when the 8,192 token limit meant that Markdown's token-efficiency over HTML was extremely worthwhile.&lt;/p&gt;
&lt;p&gt;Thariq's piece here has caused me to reconsider that, especially for output. Asking Claude for an explanation in HTML means it can drop in SVG diagrams, interactive widgets, in-page navigation and all sorts of other neat ways of making the information more pleasant to navigate.&lt;/p&gt;
&lt;p&gt;I wrote about &lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/"&gt;Useful patterns for building HTML tools&lt;/a&gt; last December, but that was focused very much on interactive utilities like the ones on my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; site. I'm excited to start experimenting more with rich HTML explanations in response to ad-hoc prompts.&lt;/p&gt;
&lt;h4 id="trying-this-out"&gt;Trying this out on copy.fail&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://copy.fail/"&gt;copy.fail&lt;/a&gt; describes a recently discovered Linux security exploit, including a proof of concept distributed as obfuscated Python.&lt;/p&gt;
&lt;p&gt;I tried having GPT-5.5 create an HTML explanation of the exploit like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;curl https://copy.fail/exp | llm -m gpt-5.5 -s 'Explain this code in detail. Reformat it, expand out any confusing bits and go deep into what it does and how it works. Output HTML, neatly styled and using capabilities of HTML and CSS and JavaScript to make the explanation rich and interactive and as clear as possible'&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gisthost.github.io/?ae53e3461ffdbfd0826156aacf025c7e"&gt;the resulting HTML page&lt;/a&gt;. It's pretty good, though I should have emphasized explaining the exploit over the Python harness around it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a dark-themed technical document titled &amp;quot;What this Python script does&amp;quot;. Body text: &amp;quot;This is a compact, deliberately obfuscated Linux-specific local privilege-escalation proof-of-concept. Its apparent goal is to tamper with the in-memory image/page cache of /usr/bin/su, then execute su to obtain elevated privileges.&amp;quot; A yellow-bordered callout reads: &amp;quot;Safety note: This explanation is for code understanding, reverse engineering, and defensive analysis. Do not run this on systems you do not own or administer. On a vulnerable kernel, code like this can alter the behavior of a privileged executable.&amp;quot; Left column heading &amp;quot;High-level summary&amp;quot;: &amp;quot;The script opens /usr/bin/su read-only, decompresses an embedded binary payload, and then processes that payload in 4-byte chunks. For each chunk, it performs a carefully arranged sequence involving Linux's kernel crypto socket interface, AF_ALG, pipes, and splice(). The important point is that this is not ordinary file writing. It never calls write() on /usr/bin/su. Instead, it appears to rely on a kernel bug/primitive involving spliced file pages and the crypto API to get controlled bytes placed into the page-cache representation of a privileged executable.&amp;quot; Numbered steps follow: &amp;quot;1. Open target executable — /usr/bin/su is opened read-only. 2. Decode hidden payload — A zlib-compressed hex blob is decompressed into bytes. 3. Patch in 4-byte chunks — The helper function is called repeatedly with offsets 0, 4, 8, ...&amp;quot;. Right column heading &amp;quot;Why it looks strange&amp;quot; contains a table with Pattern and Purpose columns: &amp;quot;import os as g — Short aliasing to make the script compact and harder to read. socket(38, 5, 0) — Uses raw numeric Linux constants instead of readable names. Compressed hex blob — Hides binary payload bytes and keeps the script small. splice() — Moves file-backed pages through pipes without normal user-space copying. try: recv(...) except: 0 — Triggers the kernel operation and ignores expected errors.&amp;quot;" src="https://static.simonwillison.net/static/2026/python-script-explainer.jpg" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="html"/><category term="security"/><category term="markdown"/><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="claude-code"/></entry><entry><title>Gwtar: a static efficient single-file HTML format</title><link href="https://simonwillison.net/2026/Feb/15/gwtar/#atom-tag" rel="alternate"/><published>2026-02-15T18:26:08+00:00</published><updated>2026-02-15T18:26:08+00:00</updated><id>https://simonwillison.net/2026/Feb/15/gwtar/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gwern.net/gwtar"&gt;Gwtar: a static efficient single-file HTML format&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fascinating new project from Gwern Branwen and Said Achmiz that targets the challenge of combining large numbers of assets into a single archived HTML file without that file being inconvenient to view in a browser.&lt;/p&gt;
&lt;p&gt;The key trick it uses is to fire &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/stop"&gt;window.stop()&lt;/a&gt; early in the page to prevent the browser from downloading the whole thing, then following that call with inline tar uncompressed content.&lt;/p&gt;
&lt;p&gt;It can then make HTTP range requests to fetch content from that tar data on-demand when it is needed by the page.&lt;/p&gt;
&lt;p&gt;The JavaScript that has already loaded rewrites asset URLs to point to &lt;code&gt;https://localhost/&lt;/code&gt; purely so that they will fail to load. Then it uses a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver"&gt;PerformanceObserver&lt;/a&gt; to catch those attempted loads:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let perfObserver = new PerformanceObserver((entryList, observer) =&amp;gt; {
    resourceURLStringsHandler(entryList.getEntries().map(entry =&amp;gt; entry.name));
});
perfObserver.observe({ entryTypes: [ "resource" ] });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;resourceURLStringsHandler&lt;/code&gt; callback finds the resource if it is already loaded or fetches it with an HTTP range request otherwise and then inserts the resource in the right place using a &lt;code&gt;blob:&lt;/code&gt; URL.&lt;/p&gt;
&lt;p&gt;Here's what the &lt;code&gt;window.stop()&lt;/code&gt; portion of the document looks like if you view the source:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a macOS terminal window titled &amp;quot;gw — more big.html — 123×46&amp;quot; showing the source code of a gwtar (self-extracting HTML archive) file. The visible code includes JavaScript with requestIdleCallback(getMainPageHTML);, a noscript block with warnings: a &amp;quot;js-disabled-warning&amp;quot; stating &amp;quot;This HTML page requires JavaScript to be enabled to render, as it is a self-extracting gwtar HTML file,&amp;quot; a description of gwtar as &amp;quot;a portable self-contained standalone HTML file which is designed to nevertheless support efficient lazy loading of all assets such as large media files,&amp;quot; with a link to https://gwern.net/gwtar, a &amp;quot;local-file-warning&amp;quot; with a shell command perl -ne'print $_ if $x; $x=1 if /&amp;lt;!-- GWTAR END/' &amp;amp;lt; foo.gwtar.html | tar --extract, and a &amp;quot;server-fail-warning&amp;quot; about misconfigured servers. Below the HTML closing tags and &amp;lt;!-- GWTAR END comment is binary tar archive data with the filename 2010-02-brianmoriarty-thesecretofpsalm46.html, showing null-padded tar header fields including ustar^@00root and octal size/permission values. At the bottom, a SingleFile metadata comment shows url: https://web.archive.org/web/20230512001411/http://ludix.com/moriarty/psalm46.html and saved date: Sat Jan 17 2026 19:26:49 GMT-0800 (Pacific Standard Time)." src="https://static.simonwillison.net/static/2026/gwtar.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Amusingly for an archive format it doesn't actually work if you open the file directly on your own computer. Here's what you see if you try to do that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are seeing this message, instead of the page you should be seeing, because &lt;code&gt;gwtar&lt;/code&gt; files &lt;strong&gt;cannot be opened locally&lt;/strong&gt; (due to web browser security restrictions).&lt;/p&gt;
&lt;p&gt;To open this page on your computer, use the following shell command:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;perl -ne'print $_ if $x; $x=1 if /&amp;lt;!-- GWTAR END/' &amp;lt; foo.gwtar.html | tar --extract&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then open the file &lt;code&gt;foo.html&lt;/code&gt; in any web browser.&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=47024506"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/archiving"&gt;archiving&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/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;



</summary><category term="archiving"/><category term="html"/><category term="javascript"/><category term="http-range-requests"/></entry><entry><title>I ported JustHTML from Python to JavaScript with Codex CLI and GPT-5.2 in 4.5 hours</title><link href="https://simonwillison.net/2025/Dec/15/porting-justhtml/#atom-tag" rel="alternate"/><published>2025-12-15T23:58:38+00:00</published><updated>2025-12-15T23:58:38+00:00</updated><id>https://simonwillison.net/2025/Dec/15/porting-justhtml/#atom-tag</id><summary type="html">
    &lt;p&gt;I &lt;a href="https://simonwillison.net/2025/Dec/14/justhtml/"&gt;wrote about JustHTML yesterday&lt;/a&gt; - Emil Stenström's project to build a new standards compliant HTML5 parser in pure Python code using coding agents running against the comprehensive html5lib-tests testing library. Last night, purely out of curiosity, I decided to try &lt;strong&gt;porting JustHTML from Python to JavaScript&lt;/strong&gt; with the least amount of effort possible, using Codex CLI and GPT-5.2. It worked beyond my expectations.&lt;/p&gt;
&lt;h4 id="tl-dr"&gt;TL;DR&lt;/h4&gt;
&lt;p&gt;I built &lt;a href="https://github.com/simonw/justjshtml"&gt;simonw/justjshtml&lt;/a&gt;, a dependency-free HTML5 parsing library in JavaScript which passes 9,200 tests from the html5lib-tests suite and imitates the API design of Emil's JustHTML library.&lt;/p&gt;
&lt;p&gt;It took two initial prompts and a few tiny follow-ups. &lt;a href="https://simonwillison.net/2025/Dec/11/gpt-52/"&gt;GPT-5.2&lt;/a&gt; running in &lt;a href="https://github.com/openai/codex"&gt;Codex CLI&lt;/a&gt; ran uninterrupted for several hours, burned through 1,464,295 input tokens, 97,122,176 cached input tokens and 625,563 output tokens and ended up producing 9,000 lines of fully tested JavaScript across 43 commits.&lt;/p&gt;
&lt;p&gt;Time elapsed from project idea to finished library: about 4 hours, during which I also bought and decorated a Christmas tree with family and watched the latest Knives Out movie.&lt;/p&gt;
&lt;h4 id="some-background"&gt;Some background&lt;/h4&gt;
&lt;p&gt;One of the most important contributions of the HTML5 specification ten years ago was the way it precisely specified how &lt;em&gt;invalid&lt;/em&gt; HTML should be parsed. The world is full of invalid documents and having a specification that covers those means browsers can treat them in the same way - there's no more "undefined behavior" to worry about when building parsing software.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, those invalid parsing rules are pretty complex! The free online book &lt;a href="https://htmlparser.info/"&gt;Idiosyncrasies of the HTML parser&lt;/a&gt; by Simon Pieters is an excellent deep dive into this topic, in particular &lt;a href="https://htmlparser.info/parser/"&gt;Chapter 3. The HTML parser&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Python &lt;a href="https://github.com/html5lib/html5lib-python"&gt;html5lib&lt;/a&gt; project started the &lt;a href="https://github.com/html5lib/html5lib-tests"&gt;html5lib-tests&lt;/a&gt; repository with a set of implementation-independent tests. These have since become the gold standard for interoperability testing of HTML5 parsers, and are used by projects such as &lt;a href="https://github.com/servo/servo"&gt;Servo&lt;/a&gt; which used them to help build &lt;a href="https://github.com/servo/html5ever"&gt;html5ever&lt;/a&gt;, a "high-performance browser-grade HTML5 parser" written in Rust.&lt;/p&gt;
&lt;p&gt;Emil Stenström's &lt;a href="https://github.com/EmilStenstrom/justhtml"&gt;JustHTML&lt;/a&gt; project is a pure-Python implementation of an HTML5 parser that passes the full html5lib-tests suite. Emil &lt;a href="https://friendlybit.com/python/writing-justhtml-with-coding-agents/"&gt;spent a couple of months&lt;/a&gt; working on this as a side project, deliberately picking a problem with a comprehensive existing test suite to see how far he could get with coding agents.&lt;/p&gt;
&lt;p&gt;At one point he had the agents rewrite it based on a close inspection of the Rust html5ever library. I don't know how much of this was direct translation versus inspiration (here's Emil's &lt;a href="https://news.ycombinator.com/item?id=46264195#46267059"&gt;commentary on that&lt;/a&gt;) - his project has 1,215 commits total so it appears to have included a huge amount of iteration, not just a straight port.&lt;/p&gt;
&lt;p&gt;My project &lt;strong&gt;is&lt;/strong&gt; a straight port. I instructed Codex CLI to build a JavaScript version of Emil's Python code.&lt;/p&gt;
&lt;h4 id="the-process-in-detail"&gt;The process in detail&lt;/h4&gt;
&lt;p&gt;I started with a bit of mise en place. I checked out two repos and created an empty third directory for the new project:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; &lt;span class="pl-k"&gt;~&lt;/span&gt;/dev
git clone https://github.com/EmilStenstrom/justhtml
git clone https://github.com/html5lib/html5lib-tests
mkdir justjshtml
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; justjshtml&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I started Codex CLI for GPT-5.2 like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;codex --yolo -m gpt-5.2&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;--yolo&lt;/code&gt; flag is a shortcut for &lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;, which is every bit as dangerous as it sounds.&lt;/p&gt;
&lt;p&gt;My first prompt told Codex to inspect the existing code and use it to build a specification for the new JavaScript library:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;We are going to create a JavaScript port of ~/dev/justhtml - an HTML parsing library that passes the full ~/dev/html5lib-tests test suite. It is going to have a similar API to the Python library but in JavaScript. It will have no dependencies other than raw JavaScript, hence it will work great in the browser and node.js and other environments. Start by reading ~/dev/justhtml and designing the user-facing API for the new library - create a spec.md containing your plan.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I reviewed the spec, which included a set of proposed milestones, and told it to add another:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add an early step to the roadmap that involves an initial version that parses a simple example document that is valid and returns the right results. Then add and commit the spec.md file.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/justjshtml/blob/19b8eb1f2ca80f428a3c40862d5ec05d36e5166b/spec.md"&gt;the resulting spec.md file&lt;/a&gt;. My request for that initial version became "Milestone 0.5" which looked like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Milestone 0.5 — End-to-end smoke parse (single valid document)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement the smallest end-to-end slice so the public API is real early:
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new JustHTML("&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;")&lt;/code&gt; returns a tree with the expected tag structure and text nodes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doc.toText()&lt;/code&gt; returns &lt;code&gt;"Hello"&lt;/code&gt; and &lt;code&gt;doc.errors&lt;/code&gt; is empty for this valid input.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;scripts/smoke.js&lt;/code&gt; (no deps) that runs the example and asserts the expected structure/output.&lt;/li&gt;
&lt;li&gt;Gate: &lt;code&gt;node scripts/smoke.js&lt;/code&gt; passes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then I told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Implement Milestone 0.5&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And off it went. The resulting code appeared to work so I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Configure GitHub Actions test.yml to run that on every commit, then commit and push&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ran &lt;code&gt;git init&lt;/code&gt; and created a private GitHub repository for this project at this point, and set up the local directory to push to that remote.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/justjshtml/commit/48497cbd05584c88af757a0736a2ee447468b6b3"&gt;that initial push&lt;/a&gt;. Then I told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OK do the rest, commit and push often&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that was almost it!&lt;/p&gt;
&lt;p&gt;I set my laptop to not fall asleep and left it to its devices while we went off to buy a Christmas tree. The "commit and push often" meant I could monitor its progress on my phone by refreshing &lt;a href="https://github.com/simonw/justjshtml/commits/main"&gt;the commit log on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was running this against my $20/month ChatGPT Plus account, which has a five hour token allowance window for Codex CLI. That ran out at 6:35pm and Codex paused, so I waited until the reset point at 7:14pm and typed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Keep going&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At 9:30pm it declared itself done with the following summary message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;• What’s Done

  - html5lib-tests now run in CI and pass: tokenizer 6810/6810, tree 1770/1782
    (12 skipped: #script-on), serializer 230/230, encoding 82/83 (1 skipped:
    scripted) (.github/workflows/test.yml:1).
  - Public API expanded: stream() (src/stream.js:1), CSS selectors query()/
    matches() (src/selector.js:1), and toMarkdown() (src/markdown.js:1), all
    exported via src/index.js:1.
  - Added no-deps unit scripts for selectors/stream/markdown (scripts/run-
    selector-tests.js:1, scripts/run-stream-tests.js:1, scripts/run-markdown-
    tests.js:1) and wired them into CI.
  - Fixed TreeBuilder so &amp;lt;br&amp;gt; no longer closes &amp;lt;p&amp;gt; (src/treebuilder.js:647).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a finishing touch, I had it add a playground interface so I could try out the new library in my browser. I prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add a playground.html in the top level folder which loads the necessary ES modules from ./src/... and implements the exact same functionality as seen on https://tools.simonwillison.net/justhtml but using the JavaScript library instead of Pyodide&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It fetched my &lt;a href="https://tools.simonwillison.net/justhtml"&gt;existing JustHTML playground page&lt;/a&gt; (&lt;a href="https://simonwillison.net/2025/Dec/14/justhtml/#first-impressions-of-justhtml"&gt;described here&lt;/a&gt;) using &lt;code&gt;curl&lt;/code&gt; and built a new &lt;code&gt;playground.html&lt;/code&gt; file that loaded the new JavaScript code instead. This worked &lt;em&gt;perfectly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I enabled GitHub Pages for my still-private repo which meant I could access the new playground at this URL:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonw.github.io/justjshtml/playground.html"&gt;https://simonw.github.io/justjshtml/playground.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/justjshtml-playground.jpg" alt="Screenshot of JustJSHTML Playground web application. Header reads &amp;quot;JustJSHTML Playground&amp;quot; with subtitle &amp;quot;A dependency-free JavaScript HTML5 parser - GitHub&amp;quot;. Below is a status bar showing &amp;quot;JavaScript Environment&amp;quot; with a green &amp;quot;Ready&amp;quot; badge. The main input area has &amp;quot;Paste HTML&amp;quot; and &amp;quot;Fetch from URL&amp;quot; buttons, with a text area containing HTML code: &amp;quot;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head&amp;gt; &amp;lt;title&amp;gt;Example Page&amp;lt;/title&amp;gt; &amp;lt;/head&amp;gt; &amp;lt;body&amp;gt; &amp;lt;header&amp;gt; &amp;lt;nav&amp;gt; &amp;lt;ul&amp;gt;&amp;quot;. A &amp;quot;Playground Mode&amp;quot; section shows buttons for &amp;quot;CSS Selector Query&amp;quot;, &amp;quot;Pretty Print HTML&amp;quot;, &amp;quot;Tree Structure&amp;quot;, &amp;quot;Stream Events&amp;quot;, &amp;quot;Extract Text&amp;quot;, and &amp;quot;To Markdown&amp;quot; (highlighted in purple). Below is a text field labeled &amp;quot;CSS Selector (optional - leave empty for whole document):&amp;quot; with placeholder &amp;quot;e.g., article, main, .content (or leave empty)&amp;quot; and a green &amp;quot;Convert to Markdown&amp;quot; button. The Output section has a teal header with &amp;quot;Whole document&amp;quot; badge and displays converted markdown: &amp;quot;Example Page&amp;quot; followed by &amp;quot;- [Home](/)&amp;quot; &amp;quot;- [About](/about)&amp;quot; &amp;quot;- [Contact](/contact)&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;All it needed now was some documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add a comprehensive README with full usage instructions including attribution plus how this was built plus how to use in in HTML plus how to use it in Node.js&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can &lt;a href="https://github.com/simonw/justjshtml/blob/f3a33fdb29bf97846fd017185edc8cf82783032e/README.md"&gt;read the result here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We are now at eight prompts total, running for just over four hours and I've decorated for Christmas and watched &lt;a href="https://en.wikipedia.org/wiki/Wake_Up_Dead_Man"&gt;Wake Up Dead Man&lt;/a&gt; on Netflix.&lt;/p&gt;
&lt;p&gt;According to Codex CLI:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Token usage: total=2,089,858 input=1,464,295 (+ 97,122,176 cached) output=625,563 (reasoning 437,010)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My &lt;a href="https://www.llm-prices.com/#it=2089858&amp;amp;cit=97122176&amp;amp;ot=625563&amp;amp;sel=gpt-5.2"&gt;llm-prices.com calculator&lt;/a&gt; estimates that at $29.41 if I was paying for those tokens at API prices, but they were included in my $20/month ChatGPT Plus subscription so the actual extra cost to me was zero.&lt;/p&gt;
&lt;h4 id="what-can-we-learn-from-this-"&gt;What can we learn from this?&lt;/h4&gt;
&lt;p&gt;I'm sharing this project because I think it demonstrates a bunch of interesting things about the state of LLMs in December 2025.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontier LLMs really can perform complex, multi-hour tasks with hundreds of tool calls and minimal supervision. I used GPT-5.2 for this but I have no reason to believe that Claude Opus 4.5 or Gemini 3 Pro would not be able to achieve the same thing - the only reason I haven't tried is that I don't want to burn another 4 hours of time and several million tokens on more runs.&lt;/li&gt;
&lt;li&gt;If you can reduce a problem to a robust test suite you can set a coding agent loop loose on it with a high degree of confidence that it will eventually succeed. I called this &lt;a href="https://simonwillison.net/2025/Sep/30/designing-agentic-loops/"&gt;designing the agentic loop&lt;/a&gt; a few months ago. I think it's the key skill to unlocking the potential of LLMs for complex tasks.&lt;/li&gt;
&lt;li&gt;Porting entire open source libraries from one language to another via a coding agent works extremely well.&lt;/li&gt;
&lt;li&gt;Code is so cheap it's practically free. Code that &lt;em&gt;works&lt;/em&gt; continues to carry a cost, but that cost has plummeted now that coding agents can check their work as they go.&lt;/li&gt;
&lt;li&gt;We haven't even &lt;em&gt;begun&lt;/em&gt; to unpack the etiquette and ethics around this style of development. Is it responsible and appropriate to churn out a direct port of a library like this in a few hours while watching a movie? What would it take for code built like this to be trusted in production?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll end with some open questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this library represent a legal violation of copyright of either the Rust library or the Python one?&lt;/li&gt;
&lt;li&gt;Even if this is legal, is it ethical to build a library in this way?&lt;/li&gt;
&lt;li&gt;Does this format of development hurt the open source ecosystem?&lt;/li&gt;
&lt;li&gt;Can I even assert copyright over this, given how much of the work was produced by the LLM?&lt;/li&gt;
&lt;li&gt;Is it responsible to publish software libraries built in this way?&lt;/li&gt;
&lt;li&gt;How much better would this library be if an expert team hand crafted it over the course of several months?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Update 11th January 2026&lt;/strong&gt;: I originally ended this post with just these open questions, but I've now provided &lt;a href="https://simonwillison.net/2026/Jan/11/answers/"&gt;my own answers to the questions&lt;/a&gt; in a new post.&lt;/p&gt;
    
        &lt;p&gt;Tags: &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/python"&gt;python&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/gpt-5"&gt;gpt-5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex"&gt;codex&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/november-2025-inflection"&gt;november-2025-inflection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-porting"&gt;vibe-porting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gpt"&gt;gpt&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="html"/><category term="javascript"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="gpt-5"/><category term="codex"/><category term="november-2025-inflection"/><category term="vibe-porting"/><category term="gpt"/></entry><entry><title>JustHTML is a fascinating example of vibe engineering in action</title><link href="https://simonwillison.net/2025/Dec/14/justhtml/#atom-tag" rel="alternate"/><published>2025-12-14T15:59:23+00:00</published><updated>2025-12-14T15:59:23+00:00</updated><id>https://simonwillison.net/2025/Dec/14/justhtml/#atom-tag</id><summary type="html">
    &lt;p&gt;I recently came across &lt;a href="https://github.com/EmilStenstrom/justhtml"&gt;JustHTML&lt;/a&gt;, a new Python library for parsing HTML released by Emil Stenström. It's a very interesting piece of software, both as a useful library and as a case study in sophisticated AI-assisted programming.&lt;/p&gt;
&lt;h4 id="first-impressions-of-justhtml"&gt;First impressions of JustHTML&lt;/h4&gt;
&lt;p&gt;I didn't initially know that JustHTML had been written with AI assistance at all. The README caught my eye due to some attractive characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It's pure Python. I like libraries that are pure Python (no C extensions or similar) because it makes them easy to use in less conventional Python environments, including Pyodide.&lt;/li&gt;
&lt;li&gt;"Passes all 9,200+ tests in the official &lt;a href="https://github.com/html5lib/html5lib-tests"&gt;html5lib-tests&lt;/a&gt; suite (used by browser vendors)" - this instantly caught my attention! HTML5 is a big, complicated but meticulously written specification.&lt;/li&gt;
&lt;li&gt;100% test coverage. That's not something you see every day.&lt;/li&gt;
&lt;li&gt;CSS selector queries as a feature. I built a Python library for this &lt;a href="https://github.com/simonw/soupselect"&gt;many years ago&lt;/a&gt; and I'm always interested in seeing new implementations of that pattern.&lt;/li&gt;
&lt;li&gt;html5lib has been &lt;a href="https://github.com/mozilla/bleach/issues/698"&gt;inconsistently maintained&lt;/a&gt; over the last few years, leaving me interested in potential alternatives.&lt;/li&gt;
&lt;li&gt;It's only 3,000 lines of implementation code (and another ~11,000 of tests.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was out and about without a laptop so I decided to put JustHTML through its paces on my phone. I &lt;a href="https://github.com/simonw/tools/pull/156#issue-3726212220"&gt;prompted Claude Code for web&lt;/a&gt; on my phone and had it build &lt;a href="https://tools.simonwillison.net/justhtml"&gt;this Pyodide-powered HTML tool&lt;/a&gt; for trying it out:&lt;/p&gt;
&lt;p style="text-align: center; margin-top: 1em"&gt;&lt;img src="https://static.simonwillison.net/static/2025/justhtml.jpeg" style="width:80%;" alt="Screenshot of a web app interface titled &amp;quot;Playground Mode&amp;quot; with buttons labeled &amp;quot;CSS Selector Query&amp;quot; (purple, selected), &amp;quot;Pretty Print HTML&amp;quot;, &amp;quot;Tree Structure&amp;quot;, &amp;quot;Stream Events&amp;quot;, &amp;quot;Extract Text&amp;quot;, and &amp;quot;To Markdown&amp;quot; (all gray). Below is a text field labeled &amp;quot;CSS Selector:&amp;quot; containing &amp;quot;p&amp;quot; and a green &amp;quot;Run Query&amp;quot; button. An &amp;quot;Output&amp;quot; section with dark background shows 3 matches in a green badge and displays HTML code" /&gt;&lt;/p&gt;
&lt;p&gt;This was enough for me to convince myself that the core functionality worked as advertised. It's a neat piece of code!&lt;/p&gt;
&lt;h4 id="turns-out-it-was-almost-all-built-by-llms"&gt;Turns out it was almost all built by LLMs&lt;/h4&gt;
&lt;p&gt;At this point I went looking for some more background information on the library and found Emil's blog entry about it: &lt;a href="https://friendlybit.com/python/writing-justhtml-with-coding-agents/"&gt;How I wrote JustHTML using coding agents&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Writing a full HTML5 parser is not a short one-shot problem. I have been working on this project for a couple of months on off-hours.&lt;/p&gt;
&lt;p&gt;Tooling: I used plain VS Code with Github Copilot in Agent mode. I enabled automatic approval of all commands, and then added a blacklist of commands that I always wanted to approve manually. I wrote an &lt;a href="https://github.com/EmilStenstrom/justhtml/blob/main/.github/copilot-instructions.md"&gt;agent instruction&lt;/a&gt; that told it to keep working, and don't stop to ask questions. Worked well!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Emil used several different models - an advantage of working in VS Code Agent mode rather than a provider-locked coding agent like Claude Code or Codex CLI. Claude Sonnet 3.7, Gemini 3 Pro and Claude Opus all get a mention.&lt;/p&gt;
&lt;h4 id="vibe-engineering-not-vibe-coding"&gt;Vibe engineering, not vibe coding&lt;/h4&gt;
&lt;p&gt;What's most interesting about Emil's 17 step account covering those several months of work is how much software engineering was involved, independent of typing out the actual code.&lt;/p&gt;
&lt;p&gt;I wrote about &lt;a href="https://simonwillison.net/2025/Oct/7/vibe-engineering/"&gt;vibe engineering&lt;/a&gt; a while ago as an alternative to vibe coding.&lt;/p&gt;
&lt;p&gt;Vibe coding is when you have an LLM knock out code without any semblance of code review - great for prototypes and toy projects, definitely not an approach to use for serious libraries or production code.&lt;/p&gt;
&lt;p&gt;I proposed "vibe engineering" as the grown up version of vibe coding, where expert programmers use coding agents in a professional and responsible way to produce high quality, reliable results.&lt;/p&gt;
&lt;p&gt;You should absolutely read &lt;a href="https://friendlybit.com/python/writing-justhtml-with-coding-agents/#the-journey"&gt;Emil's account&lt;/a&gt; in full. A few highlights:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;He hooked in the 9,200 test &lt;a href="https://github.com/html5lib/html5lib-tests"&gt;html5lib-tests&lt;/a&gt; conformance suite almost from the start. There's no better way to construct a new HTML5 parser than using the test suite that the browsers themselves use.&lt;/li&gt;
&lt;li&gt;He picked the core API design himself - a TagHandler base class with handle_start() etc. methods - and told the model to implement that.&lt;/li&gt;
&lt;li&gt;He added a comparative benchmark to track performance compared to existing libraries like html5lib, then experimented with a Rust optimization based on those initial numbers.&lt;/li&gt;
&lt;li&gt;He threw the original code away and started from scratch as a rough port of Servo's excellent &lt;a href="https://github.com/servo/html5ever"&gt;html5ever&lt;/a&gt; Rust library.&lt;/li&gt;
&lt;li&gt;He built a custom profiler and new benchmark and let Gemini 3 Pro loose on it, finally achieving micro-optimizations to beat the existing Pure Python libraries.&lt;/li&gt;
&lt;li&gt;He used coverage to identify and remove unnecessary code.&lt;/li&gt;
&lt;li&gt;He had his agent build a &lt;a href="https://github.com/EmilStenstrom/justhtml/blob/main/benchmarks/fuzz.py"&gt;custom fuzzer&lt;/a&gt; to generate vast numbers of invalid HTML documents and harden the parser against them.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This represents a lot of sophisticated development practices, tapping into Emil's deep experience as a software engineer. As described, this feels to me more like a lead architect role than a hands-on coder.&lt;/p&gt;
&lt;p&gt;It perfectly fits what I was thinking about when I described &lt;strong&gt;vibe engineering&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Setting the coding agent up with the html5lib-tests suite is also a great example of &lt;a href="https://simonwillison.net/2025/Sep/30/designing-agentic-loops/"&gt;designing an agentic loop&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="-the-agent-did-the-typing-"&gt;"The agent did the typing"&lt;/h4&gt;
&lt;p&gt;Emil concluded his article like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JustHTML is about 3,000 lines of Python with 8,500+ tests passing. I couldn't have written it this quickly without the agent.&lt;/p&gt;
&lt;p&gt;But "quickly" doesn't mean "without thinking." I spent a lot of time reviewing code, making design decisions, and steering the agent in the right direction. The agent did the typing; I did the thinking.&lt;/p&gt;
&lt;p&gt;That's probably the right division of labor.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I couldn't agree more. Coding agents replace the part of my job that involves typing the code into a computer. I find what's left to be a much more valuable use of my time.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&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/vibe-coding"&gt;vibe-coding&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/conformance-suites"&gt;conformance-suites&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="html"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="vibe-coding"/><category term="coding-agents"/><category term="conformance-suites"/></entry><entry><title>Useful patterns for building HTML tools</title><link href="https://simonwillison.net/2025/Dec/10/html-tools/#atom-tag" rel="alternate"/><published>2025-12-10T21:00:59+00:00</published><updated>2025-12-10T21:00:59+00:00</updated><id>https://simonwillison.net/2025/Dec/10/html-tools/#atom-tag</id><summary type="html">
    &lt;p&gt;I've started using the term &lt;strong&gt;HTML tools&lt;/strong&gt; to refer to HTML applications that I've been building which combine HTML, JavaScript, and CSS in a single file and use them to provide useful functionality. I have built &lt;a href="https://tools.simonwillison.net/"&gt;over 150 of these&lt;/a&gt; in the past two years, almost all of them written by LLMs. This article presents a collection of useful patterns I've discovered along the way.&lt;/p&gt;
&lt;p&gt;First, some examples to show the kind of thing I'm talking about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/svg-render?url=https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg"&gt;svg-render&lt;/a&gt;&lt;/strong&gt; renders SVG code to downloadable JPEGs or PNGs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pypi-changelog?package=llm&amp;amp;compare=0.27...0.27.1"&gt;pypi-changelog&lt;/a&gt;&lt;/strong&gt; lets you generate (and copy to clipboard) diffs between different PyPI package releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/bluesky-thread?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7gzjew3ss2e&amp;amp;view=thread"&gt;bluesky-thread&lt;/a&gt;&lt;/strong&gt; provides a nested view of a discussion thread on Bluesky.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/svg-render?url=https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/svg-render.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of svg-render" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/pypi-changelog?package=llm&amp;amp;compare=0.27...0.27.1" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/pypi-changelog.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of pypi-changelog" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/bluesky-thread?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7gzjew3ss2e&amp;amp;view=thread" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/bluesky-thread.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of bluesky-thread" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;These are some of my recent favorites. I have dozens more like this that I use on a regular basis.&lt;/p&gt;
&lt;p&gt;You can explore my collection on &lt;strong&gt;&lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt;&lt;/strong&gt; - the &lt;a href="https://tools.simonwillison.net/by-month"&gt;by month&lt;/a&gt; view is useful for browsing the entire collection.&lt;/p&gt;
&lt;p&gt;If you want to see the code and prompts, almost all of the examples in this post include a link in their footer to "view source" on GitHub. The GitHub commits usually contain either the prompt itself or a link to the transcript used to create the tool.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#the-anatomy-of-an-html-tool"&gt;The anatomy of an HTML tool&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#prototype-with-artifacts-or-canvas"&gt;Prototype with Artifacts or Canvas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#switch-to-a-coding-agent-for-more-complex-projects"&gt;Switch to a coding agent for more complex projects&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#load-dependencies-from-cdns"&gt;Load dependencies from CDNs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#host-them-somewhere-else"&gt;Host them somewhere else&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#take-advantage-of-copy-and-paste"&gt;Take advantage of copy and paste&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#build-debugging-tools"&gt;Build debugging tools&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#persist-state-in-the-url"&gt;Persist state in the URL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#use-localstorage-for-secrets-or-larger-state"&gt;Use localStorage for secrets or larger state&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#collect-cors-enabled-apis"&gt;Collect CORS-enabled APIs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#llms-can-be-called-directly-via-cors"&gt;LLMs can be called directly via CORS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#don-t-be-afraid-of-opening-files"&gt;Don't be afraid of opening files&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#you-can-offer-downloadable-files-too"&gt;You can offer downloadable files too&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#pyodide-can-run-python-code-in-the-browser"&gt;Pyodide can run Python code in the browser&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#webassembly-opens-more-possibilities"&gt;WebAssembly opens more possibilities&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#remix-your-previous-tools"&gt;Remix your previous tools&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#record-the-prompt-and-transcript"&gt;Record the prompt and transcript&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#go-forth-and-build"&gt;Go forth and build&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="the-anatomy-of-an-html-tool"&gt;The anatomy of an HTML tool&lt;/h4&gt;
&lt;p&gt;These are the characteristics I have found to be most productive in building tools of this nature:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A single file: inline JavaScript and CSS in a single HTML file means the least hassle in hosting or distributing them, and crucially means you can copy and paste them out of an LLM response.&lt;/li&gt;
&lt;li&gt;Avoid React, or anything with a build step. The problem with React is that JSX requires a build step, which makes everything massively less convenient. I prompt "no react" and skip that whole rabbit hole entirely.&lt;/li&gt;
&lt;li&gt;Load dependencies from a CDN. The fewer dependencies the better, but if there's a well known library that helps solve a problem I'm happy to load it from CDNjs or jsdelivr or similar.&lt;/li&gt;
&lt;li&gt;Keep them small. A few hundred lines means the maintainability of the code doesn't matter too much: any good LLM can read them and understand what they're doing, and rewriting them from scratch with help from an LLM takes just a few minutes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The end result is a few hundred lines of code that can be cleanly copied and pasted into a GitHub repository.&lt;/p&gt;
&lt;h4 id="prototype-with-artifacts-or-canvas"&gt;Prototype with Artifacts or Canvas&lt;/h4&gt;
&lt;p&gt;The easiest way to build one of these tools is to start in ChatGPT or Claude or Gemini. All three have features where they can write a simple HTML+JavaScript application and show it to you directly.&lt;/p&gt;
&lt;p&gt;Claude calls this "Artifacts", ChatGPT and Gemini both call it "Canvas". Claude has the feature enabled by default, ChatGPT and Gemini may require you to toggle it on in their "tools" menus.&lt;/p&gt;
&lt;p&gt;Try this prompt in Gemini or ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a canvas that lets me paste in JSON and converts it to YAML. No React.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or this prompt in Claude:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build an artifact that lets me paste in JSON and converts it to YAML. No React.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I always add "No React" to these prompts, because otherwise they tend to build with React, resulting in a file that is harder to copy and paste out of the LLM and use elsewhere. I find that attempts which use React take longer to display (since they need to run a build step) and are more likely to contain crashing bugs for some reason, especially in ChatGPT.&lt;/p&gt;
&lt;p&gt;All three tools have "share" links that provide a URL to the finished application. Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chatgpt.com/canvas/shared/6938e8ece53c8191a2f9d7dfcd090d11"&gt;ChatGPT JSON to YAML Canvas&lt;/a&gt; made with GPT-5.1 Thinking - here's &lt;a href="https://chatgpt.com/share/6938e926-ee14-8006-9678-383b3a8dac78"&gt;the full ChatGPT transcript&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/public/artifacts/61fdecb8-6e3b-4162-a5ab-6720dfe5ed19"&gt;Claude JSON to YAML Artifact&lt;/a&gt; made with Claude Opus 4.5 - here's &lt;a href="https://claude.ai/share/421bacb9-54b4-45b4-b41c-a436bc0ebd53"&gt;the full Claude transcript&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gemini.google.com/share/03c1ac87aa40"&gt;Gemini JSON to YAML Canvas&lt;/a&gt; made with Gemini 3 Pro - here's &lt;a href="https://gemini.google.com/share/1e27a1d8cdca"&gt;the full Gemini transcript&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="switch-to-a-coding-agent-for-more-complex-projects"&gt;Switch to a coding agent for more complex projects&lt;/h4&gt;
&lt;p&gt;Coding agents such as Claude Code and Codex CLI have the advantage that they can test the code themselves while they work on it using tools like Playwright. I often upgrade to one of those when I'm working on something more complicated, like my Bluesky thread viewer tool shown above.&lt;/p&gt;
&lt;p&gt;I also frequently use &lt;strong&gt;asynchronous coding agents&lt;/strong&gt; like Claude Code for web to make changes to existing tools. I shared a video about that in &lt;a href="https://simonwillison.net/2025/Oct/23/claude-code-for-web-video/"&gt;Building a tool to copy-paste share terminal sessions using Claude Code for web&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Claude Code for web and Codex Cloud run directly against my &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; repo, which means they can publish or upgrade tools via Pull Requests (here are &lt;a href="https://github.com/simonw/tools/pulls?q=is%3Apr+is%3Aclosed"&gt;dozens of examples&lt;/a&gt;) without me needing to copy and paste anything myself.&lt;/p&gt;
&lt;h4 id="load-dependencies-from-cdns"&gt;Load dependencies from CDNs&lt;/h4&gt;
&lt;p&gt;Any time I use an additional JavaScript library as part of my tool I like to load it from a CDN.&lt;/p&gt;
&lt;p&gt;The three major LLM platforms support specific CDNs as part of their Artifacts or Canvas features, so often if you tell them "Use PDF.js" or similar they'll be able to compose a URL to a CDN that's on their allow-list.&lt;/p&gt;
&lt;p&gt;Sometimes you'll need to go and look up the URL on &lt;a href="https://cdnjs.com/"&gt;cdnjs&lt;/a&gt; or &lt;a href="https://www.jsdelivr.com/"&gt;jsDelivr&lt;/a&gt; and paste it into the chat.&lt;/p&gt;
&lt;p&gt;CDNs like these have been around for long enough that I've grown to trust them, especially for URLs that include the package version.&lt;/p&gt;
&lt;p&gt;The alternative to CDNs is to use npm and have a build step for your projects. I find this reduces my productivity at hacking on individual tools and makes it harder to self-host them.&lt;/p&gt;
&lt;h4 id="host-them-somewhere-else"&gt;Host them somewhere else&lt;/h4&gt;
&lt;p&gt;I don't like leaving my HTML tools hosted by the LLM platforms themselves for a couple of reasons. First, LLM platforms tend to run the tools inside a tight sandbox with a lot of restrictions. They're often unable to load data or images from external URLs, and sometimes even features like linking out to other sites are disabled.&lt;/p&gt;
&lt;p&gt;The end-user experience often isn't great either. They show warning messages to new users, often take additional time to load and delight in showing promotions for the platform that was used to create the tool.&lt;/p&gt;
&lt;p&gt;They're also not as reliable as other forms of static hosting. If ChatGPT or Claude are having an outage I'd like to still be able to access the tools I've created in the past.&lt;/p&gt;
&lt;p&gt;Being able to easily self-host is the main reason I like insisting on "no React" and using CDNs for dependencies - the absence of a build step makes hosting tools elsewhere a simple case of copying and pasting them out to some other provider.&lt;/p&gt;
&lt;p&gt;My preferred provider here is &lt;a href="https://docs.github.com/en/pages"&gt;GitHub Pages&lt;/a&gt; because I can paste a block of HTML into a file on github.com and have it hosted on a permanent URL a few seconds later. Most of my tools end up in my &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; repository which is configured to serve static files at &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="take-advantage-of-copy-and-paste"&gt;Take advantage of copy and paste&lt;/h4&gt;
&lt;p&gt;One of the most useful input/output mechanisms for HTML tools comes in the form of &lt;strong&gt;copy and paste&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I frequently build tools that accept pasted content, transform it in some way and let the user copy it back to their clipboard to paste somewhere else.&lt;/p&gt;
&lt;p&gt;Copy and paste on mobile phones is fiddly, so I frequently include "Copy to clipboard" buttons that populate the clipboard with a single touch.&lt;/p&gt;
&lt;p&gt;Most operating system clipboards can carry multiple formats of the same copied data. That's why you can paste content from a word processor in a way that preserves formatting, but if you paste the same thing into a text editor you'll get the content with formatting stripped.&lt;/p&gt;
&lt;p&gt;These rich copy operations are available in JavaScript paste events as well, which opens up all sorts of opportunities for HTML tools.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/hacker-news-thread-export"&gt;hacker-news-thread-export&lt;/a&gt;&lt;/strong&gt; lets you paste in a URL to a Hacker News thread and gives you a copyable condensed version of the entire thread, suitable for pasting into an LLM to get a useful summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/paste-rich-text"&gt;paste-rich-text&lt;/a&gt;&lt;/strong&gt; lets you copy from a page and paste to get the HTML - particularly useful on mobile where view-source isn't available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/alt-text-extractor"&gt;alt-text-extractor&lt;/a&gt;&lt;/strong&gt; lets you paste in images and then copy out their alt text.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/hacker-news-thread-export" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/hacker-news-thread-export.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of hacker-news-thread-export" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/paste-rich-text" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/paste-rich-text.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of paste-rich-text" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/alt-text-extractor" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/alt-text-extractor.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of alt-text-extractor" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="build-debugging-tools"&gt;Build debugging tools&lt;/h4&gt;
&lt;p&gt;The key to building interesting HTML tools is understanding what's possible. Building custom debugging tools is a great way to explore these options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/clipboard-viewer"&gt;clipboard-viewer&lt;/a&gt;&lt;/strong&gt; is one of my most useful. You can paste anything into it (text, rich text, images, files) and it will loop through and show you every type of paste data that's available on the clipboard.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/clipboard-viewer.jpg" alt="Clipboard Format Viewer. Paste anywhere on the page (Ctrl+V or Cmd+V). This shows text/rtf with a bunch of weird code, text/plain with some pasted HTML diff and a Clipboard Event Information panel that says Event type: paste, Formats available: text/rtf, text/plain, 0 files reported and 2 clipboard items reported." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This was key to building many of my other tools, because it showed me the invisible data that I could use to bootstrap other interesting pieces of functionality.&lt;/p&gt;
&lt;p&gt;More debugging examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/keyboard-debug"&gt;keyboard-debug&lt;/a&gt;&lt;/strong&gt; shows the keys (and &lt;code&gt;KeyCode&lt;/code&gt; values) currently being held down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/cors-fetch"&gt;cors-fetch&lt;/a&gt;&lt;/strong&gt; reveals if a URL can be accessed via CORS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/exif"&gt;exif&lt;/a&gt;&lt;/strong&gt; displays EXIF data for a selected photo.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/keyboard-debug" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/keyboard-debug.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of keyboard-debug" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/cors-fetch" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/cors-fetch.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of cors-fetch" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/exif" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/exif.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of exif" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="persist-state-in-the-url"&gt;Persist state in the URL&lt;/h4&gt;
&lt;p&gt;HTML tools may not have access to server-side databases for storage but it turns out you can store a &lt;em&gt;lot&lt;/em&gt; of state directly in the URL.&lt;/p&gt;
&lt;p&gt;I like this for tools I may want to bookmark or share with other people.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/icon-editor#cmdiKDIwMSwgNDYsIDg2KSxyZ2IoMjIzLCA0OCwgOTIpLHJnYigzNCwgODAsIDE3OSkscmdiKDIzNywgNTYsIDk1KSxyZ2IoMTgzLCA1MywgOTYpLHJnYigzOCwgMTA3LCAyMTApLHJnYigyMDQsIDY1LCAxMDUpLHJnYigxNzksIDEwMywgMTM2KSxyZ2IoMjMyLCA5NywgMTQ4KSxyZ2IoMzgsIDkxLCAyMDkpLHJnYigzNiwgOTUsIDIwNCkscmdiKDE5NSwgODYsIDEyOSkscmdiKDE3MywgMzEsIDU4KSxyZ2IoMjEyLCA2MSwgMTA2KSxyZ2IoOTIsIDEwNSwgMTg4KSxyZ2IoMjM3LCA3MSwgMTIzKSxyZ2IoMzksIDk2LCAyMTkpLHJnYigyOCwgODYsIDIxMCkscmdiKDIyMywgMjEyLCAzNCkscmdiKDE3MywgMTUzLCAyNikscmdiKDE0NCwgNzksIDI4KSxyZ2IoMjI0LCA1NiwgOTcpLHJnYigxOTYsIDQ4LCA4NSkscmdiKDIyMCwgNTAsIDk4KSxyZ2IoMTY2LCAxMjYsIDI1KSxyZ2IoMjA5LCAxMzAsIDE5KSxyZ2IoMTg3LCAxMTQsIDEzKSxyZ2IoMTQ3LCAxMDQsIDE4KSxyZ2IoMjE2LCA1OCwgODEpLHJnYigxNTIsIDM5LCA2NCkscmdiKDMyLCA3NSwgMTczKSxyZ2IoMTY2LCAxMjYsIDI5KSxyZ2IoMjM3LCAxODAsIDU0KSxyZ2IoMjA0LCAxMzgsIDIyKSxyZ2IoMTgxLCAxMjksIDIzKSxyZ2IoMjM0LCA4NiwgNzYpLHJnYigxOTAsIDY4LCA3NSkscmdiKDI0NSwgODksIDEzNSkscmdiKDIxMywgNjcsIDExMSkscmdiKDE0MSwgMzEsIDU2KSxyZ2IoNzIsIDc5LCAxMTYpLHJnYigxODcsIDE1NCwgNTIpLHJnYigyMDcsIDE3OSwgNzIpLHJnYigyMTAsIDE2MiwgNDMpLHJnYigyMTQsIDE0OSwgMzEpLHJnYigyMzksIDkwLCA4NCkscmdiKDIzNSwgMTMyLCA3NykscmdiKDE4MSwgMTM4LCAyOSkscmdiKDI0NSwgMTI4LCAxNzgpLHJnYigyMTcsIDk5LCAxNDUpLHJnYigxMTYsIDEwNSwgMTIyKSxyZ2IoMjA2LCAxNzYsIDY1KSxyZ2IoMTkxLCAxNjMsIDY0KSxyZ2IoMjA1LCAxNjksIDU4KSxyZ2IoMjM2LCAxNjUsIDQ2KSxyZ2IoMjM3LCA3OSwgODUpLHJnYigyMzUsIDE0NCwgODcpLHJnYigyNDksIDIwMiwgNDUpLHJnYigyMTAsIDE2NiwgMzQpLHJnYigyMjcsIDEwMywgMTYyKSxyZ2IoMjEzLCA5MCwgMTMwKSxyZ2IoNDQsIDQ4LCAxMjMpLHJnYigxMjUsIDg2LCAxNTEpLHJnYigxOTAsIDE2MywgMTA2KSxyZ2IoMTk5LCAxNjYsIDQ4KSxyZ2IoMjAyLCAxNjQsIDU2KSxyZ2IoMjIxLCAxNzAsIDUzKSxyZ2IoMjM0LCAxMzUsIDc1KSxyZ2IoMjQxLCAxNzUsIDc1KSxyZ2IoMjU1LCAyMjIsIDY1KSxyZ2IoMjU0LCAyMjYsIDY5KSxyZ2IoMjM1LCAyMDEsIDQ0KSxyZ2IoNzMsIDEzNywgMjQ3KSxyZ2IoODAsIDE0MywgMjQ4KSxyZ2IoNzksIDEzOSwgMjQzKSxyZ2IoMTM4LCA5MiwgMTc0KSxyZ2IoMTU2LCAxMTMsIDE3NikscmdiKDIwMSwgMTY4LCA2MykscmdiKDIxMSwgMTY5LCA0NikscmdiKDIxNCwgMTcxLCA1NSkscmdiKDIyOCwgMTgyLCA1NikscmdiKDI0MywgMTk1LCA1OCkscmdiKDI0NSwgMjA0LCA2NykscmdiKDI1NSwgMjIxLCA2NykscmdiKDI1NSwgMjI2LCA2OCkscmdiKDE1NCwgMTYyLCAxMzMpLHJnYigyNiwgMTA1LCAyNTUpLHJnYig2OCwgMTI5LCAyNTIpLHJnYig4NywgMTM1LCAyNDQpLHJnYig4MywgMTMxLCAyMzUpLHJnYig4MiwgMTI3LCAyMjYpLHJnYig4NSwgMTMwLCAyMjcpLHJnYig3OSwgMTIyLCAyMTgpLHJnYigxNjcsIDE0NiwgMzIpLHJnYigxNzQsIDEzOCwgMTI0KSxyZ2IoMTMzLCA2OSwgMjA1KSxyZ2IoMTcxLCAxMjAsIDE0NCkscmdiKDIxNSwgMTc2LCA1NykscmdiKDIyMCwgMTc1LCA0OSkscmdiKDIyMywgMTc5LCA1OCkscmdiKDIzNywgMTg4LCA2MCkscmdiKDI0MSwgMTkxLCA1NikscmdiKDIwMCwgMTc2LCAxMDUpLHJnYigxMTIsIDE0MSwgMjAzKSxyZ2IoODQsIDEyNywgMjM1KSxyZ2IoMTE1LCAxMzgsIDE5MSkscmdiKDgyLCAxMDMsIDE3NCkscmdiKDE1OCwgNDEsIDc2KSxyZ2IoMTcwLCA0MywgNjQpLHJnYigxOTAsIDE1NywgNTApLHJnYigyMDMsIDE3NywgNjUpLHJnYigxNjEsIDEwMiwgMTQyKSxyZ2IoMTQxLCA1OSwgMjA5KSxyZ2IoMTgwLCAxMjIsIDE1MSkscmdiKDIyOCwgMTg1LCA1OCkscmdiKDIzMywgMTg2LCA1MikscmdiKDI0MCwgMTg5LCA2NikscmdiKDI1NCwgMjEwLCA2OCkscmdiKDIwMSwgMTkxLCAxMTMpLHJnYigxMzcsIDEzOSwgMTU3KSxyZ2IoMjExLCAxNjIsIDg4KSxyZ2IoMjUwLCAyMDAsIDUwKSxyZ2IoMTc5LCAxMzEsIDIzKSxyZ2IoMTk2LCAxNjUsIDY0KSxyZ2IoMjA1LCAxNzQsIDU0KSxyZ2IoMjA5LCAxNjAsIDU5KSxyZ2IoMTY2LCA5MSwgMTYxKSxyZ2IoMTQyLCA2MCwgMjIzKSxyZ2IoMTk3LCAxMzksIDE1MCkscmdiKDI0MCwgMTk2LCA3MikscmdiKDI1MSwgMjA4LCA2MSkscmdiKDI1NSwgMjI0LCA4MCkscmdiKDI1NSwgMjUwLCA5MikscmdiKDI1NSwgMjM0LCA4OSkscmdiKDI0OSwgMTg2LCA1MSkscmdiKDI1MCwgMTgwLCAzOSkscmdiKDI0MCwgMTY2LCAzNSkscmdiKDIwMiwgMTc0LCA3MikscmdiKDIxNSwgMTY4LCA1MCkscmdiKDIyMiwgMTc1LCA0MykscmdiKDIxMiwgMTY1LCA2OSkscmdiKDE3NCwgMTAzLCAxNjcpLHJnYigxNjAsIDc4LCAyMzQpLHJnYigyMDUsIDE0NiwgMTg0KSxyZ2IoMjQ3LCAyMTgsIDEwOCkscmdiKDI1NSwgMjQ4LCA4NSkscmdiKDI1NSwgMjU1LCAxMDIpLHJnYigyNTUsIDI1NSwgMTIyKSxyZ2IoMjQwLCAyMTAsIDgyKSxyZ2IoMjE0LCAxNTAsIDMxKSxyZ2IoMjI0LCAxNTAsIDI1KSxyZ2IoMTc2LCAxMjEsIDI1KSxyZ2IoMTg5LCAxODMsIDUyKSxyZ2IoMTIyLCA4MCwgMTU4KSxyZ2IoMTkxLCAxNTEsIDEyMikscmdiKDIyOSwgMTc0LCA0MCkscmdiKDIyNSwgMTcyLCA1MSkscmdiKDIyOSwgMTg1LCA1MSkscmdiKDIzNywgMTkwLCA2MCkscmdiKDIwOSwgMTQ2LCAxNjEpLHJnYigxOTUsIDExNywgMjUxKSxyZ2IoMjI1LCAxNTUsIDIzOSkscmdiKDI1NCwgMjI3LCAxODQpLHJnYigyNTUsIDI1NSwgMTE3KSxyZ2IoMjQ5LCAyMzcsIDc2KSxyZ2IoMjA0LCAxNjcsIDU1KSxyZ2IoMTU3LCAxMTUsIDI1KSxyZ2IoMTM1LCA5OCwgMTYpLHJnYigyMDMsIDEyNSwgNTcpLHJnYigxOTgsIDEyNSwgNTMpLHJnYigxNTcsIDExMCwgMTQ0KSxyZ2IoMTQ5LCA4NCwgMTk0KSxyZ2IoMjEyLCAxNTcsIDk0KSxyZ2IoMjMyLCAxODUsIDQ3KSxyZ2IoMjM1LCAxODYsIDYyKSxyZ2IoMjUwLCAyMDQsIDY1KSxyZ2IoMjUzLCAyMzIsIDgxKSxyZ2IoMjQzLCAyMTUsIDE0OCkscmdiKDI0NywgMTgzLCAyMzMpLHJnYigyNDMsIDE2MywgMjUwKSxyZ2IoMTk4LCAxMzgsIDE3NSkscmdiKDE2MCwgMTEzLCA4MikscmdiKDEyNCwgODksIDM3KSxyZ2IoMTU3LCAxMzYsIDM2KSxyZ2IoMjAzLCAxNjQsIDgyKSxyZ2IoMTQ4LCA3MiwgMTg5KSxyZ2IoMTU4LCA4NCwgMjA0KSxyZ2IoMjE3LCAxNjgsIDExNykscmdiKDI1MCwgMjEwLCA2NykscmdiKDI1NSwgMjI5LCA3OCkscmdiKDI1NSwgMjU1LCA5NikscmdiKDI1NSwgMjU1LCA5NCkscmdiKDI0MywgMjE4LCA5NSkscmdiKDE3OCwgMTE4LCAxMDYpLHJnYigxMDMsIDQwLCAxMDIpLHJnYigxODgsIDExMSwgMjcpLHJnYigxODMsIDE1NiwgNTkpLHJnYigyMTUsIDE3NiwgNDgpLHJnYigyMDMsIDE0OCwgOTEpLHJnYigxNjcsIDg5LCAxOTcpLHJnYigxNzgsIDEwMywgMjM1KSxyZ2IoMjM1LCAxOTMsIDE3NSkscmdiKDI1NSwgMjUxLCAxMjQpLHJnYigyNDksIDI0MCwgOTIpLHJnYigyMTMsIDE4NiwgNjApLHJnYigxNjAsIDEyMSwgMjEpLHJnYigxOTEsIDE1NSwgMTA4KSxyZ2IoMjIxLCAxODAsIDQwKSxyZ2IoMjM3LCAxODksIDQ3KSxyZ2IoMjMzLCAxODYsIDk2KSxyZ2IoMjE5LCAxNjIsIDIwNykscmdiKDIzMSwgMTU5LCAyNDkpLHJnYigyMTAsIDE1OCwgMTkxKSxyZ2IoMTY5LCAxMzAsIDc1KSxyZ2IoMTQwLCA5NiwgMTE5KSxyZ2IoMTU1LCA4NSwgMjAwKSxyZ2IoMjA5LCAxNTcsIDExNSkscmdiKDI1NCwgMjI2LCA3MCkscmdiKDI1NSwgMjU1LCA4MCkscmdiKDIzNSwgMjE3LCA3NikscmdiKDE3OCwgMTMzLCA5MSkscmdiKDIwOSwgMTEwLCAxNTEpLHJnYigxNTIsIDExOCwgNTYpLHJnYigxODYsIDExNiwgMTY4KSxyZ2IoMTkzLCAxMjEsIDIzNikscmdiKDIyOSwgMTk1LCAxNjEpLHJnYigxOTcsIDE4MCwgNzUpLHJnYigxOTksIDE1OCwgNzApLHJnYigxOTcsIDE0OCwgMTM2KXxfX19fX19fXzAxX19fX19fX19fX19fX19fMl9fX19fX18zNDVfX19fX182X183OF9fOWFfX19fX2JjZGVfX19fX19fX19fZl9fX2doX2lqa19fbF9fX19fX19fbV9uX19fX19fX19vcHFyc19fX19fX19fdF9fX19fX3VfX192d3h5ejEwX19fMTExMl9fMTNfX19fX19fX18xNDE1MTYxNzE4MTkxYTFiX18xYzFkX19fX19fX19fX19fMWUxZjFnMWgxaTFqMWsxbDFtXzFuMW9fX19fX19fX19fXzFwMXExcjFzMXQxdTF2MXcxeDF5MXpfX19fXzIwMjEyMl9fX19fXzIzMjQyNTI2MjcyODI5MmEyYjJjMmQyZTJmMmcyaDJpMmoya19fX19fMmwybTJuMm8ycDJxMnIyczJ0MnUydjJ3MngyeV9fX19fX19fMnozMDMxMzIzMzM0MzUzNjM3MzgzOTNhM2IzYzNkM2VfX19fX19fX19fM2YzZzNoM2kzajNrM2wzbTNuM28zcDNxM3Izc19fX19fX19fX18zdDN1M3YzdzN4M3kzejQwNDE0MjQzNDQ0NTQ2NDc0OF9fX19fX180OTRhNGI0YzRkNGU0ZjRnNGg0aTRqNGs0bDRtNG5fX180bzRwX19fXzRxNHI0czR0NHU0djR3NHg0eTR6NTA1MTUyX19fX19fX19fXzUzNTQ1NTU2NTc1ODU5NWE1YjVjNWQ1ZV9fX19fXzVmX19fX181ZzVoNWk1ajVrNWw1bTVuNW81cF9fX19fX19fX19fX19fNXE1cjVzNXQ1dTV2NXc1eF9fX19fX19fX19fX19fXzV5NXo2MDYxNjI2MzY0X19fX19fX19fX19fNjVfX19fNjY2NzY4Njk2YV9fX19fX19fX19fX19fX19fX19fNmI2Y19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19f"&gt;icon-editor&lt;/a&gt;&lt;/strong&gt; is a custom 24x24 icon editor I built to help hack on icons for &lt;a href="https://simonwillison.net/2025/Oct/28/github-universe-badge/"&gt;the GitHub Universe badge&lt;/a&gt;. It persists your in-progress icon design in the URL so you can easily bookmark and share it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="use-localstorage-for-secrets-or-larger-state"&gt;Use localStorage for secrets or larger state&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"&gt;localStorage&lt;/a&gt; browser API lets HTML tools store data persistently on the user's device, without exposing that data to the server.&lt;/p&gt;
&lt;p&gt;I use this for larger pieces of state that don't fit comfortably in a URL, or for secrets like API keys which I really don't want anywhere near my server  - even static hosts might have server logs that are outside of my influence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/word-counter"&gt;word-counter&lt;/a&gt;&lt;/strong&gt; is a simple tool I built to help me write to specific word counts, for things like conference abstract submissions. It uses localStorage to save as you type, so your work isn't lost if you accidentally close the tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/render-markdown"&gt;render-markdown&lt;/a&gt;&lt;/strong&gt; uses the same trick - I sometimes use this one to craft blog posts and I don't want to lose them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/haiku"&gt;haiku&lt;/a&gt;&lt;/strong&gt; is one of a number of LLM demos I've built that request an API key from the user (via the &lt;code&gt;prompt()&lt;/code&gt; function) and then store that in &lt;code&gt;localStorage&lt;/code&gt;. This one uses Claude Haiku to write haikus about what it can see through the user's webcam.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/word-counter" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/word-counter.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of word-counter" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/render-markdown" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/render-markdown.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of render-markdown" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/haiku" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/haiku.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of haiku" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="collect-cors-enabled-apis"&gt;Collect CORS-enabled APIs&lt;/h4&gt;
&lt;p&gt;CORS stands for &lt;a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing"&gt;Cross-origin resource sharing&lt;/a&gt;. It's a relatively low-level detail which controls if JavaScript running on one site is able to fetch data from APIs hosted on other domains.&lt;/p&gt;
&lt;p&gt;APIs that provide open CORS headers are a goldmine for HTML tools. It's worth building a collection of these over time.&lt;/p&gt;
&lt;p&gt;Here are some I like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iNaturalist for fetching sightings of animals, including URLs to photos&lt;/li&gt;
&lt;li&gt;PyPI for fetching details of Python packages&lt;/li&gt;
&lt;li&gt;GitHub because anything in a public repository in GitHub has a CORS-enabled anonymous API for fetching that content from the raw.githubusercontent.com domain, which is behind a caching CDN so you don't need to worry too much about rate limits or feel guilty about adding load to their infrastructure.&lt;/li&gt;
&lt;li&gt;Bluesky for all sorts of operations&lt;/li&gt;
&lt;li&gt;Mastodon has generous CORS policies too, as used by applications like &lt;a href="https://phanpy.social/"&gt;phanpy.social&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub Gists are a personal favorite here, because they let you build apps that can persist state to a permanent Gist through making a cross-origin API call.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/species-observation-map#%7B%22taxonId%22%3A123829%2C%22taxonName%22%3A%22California%20Brown%20Pelican%22%2C%22days%22%3A%2230%22%7D"&gt;species-observation-map&lt;/a&gt;&lt;/strong&gt; uses iNaturalist to show a map of recent sightings of a particular species.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?package=llm"&gt;zip-wheel-explorer&lt;/a&gt;&lt;/strong&gt; fetches a &lt;code&gt;.whl&lt;/code&gt; file for a Python package from PyPI, unzips it (in browser memory) and lets you navigate the files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/github-issue-to-markdown?issue=https%3A%2F%2Fgithub.com%2Fsimonw%2Fsqlite-utils%2Fissues%2F657"&gt;github-issue-to-markdown&lt;/a&gt;&lt;/strong&gt; fetches issue details and comments from the GitHub API (including expanding any permanent code links) and turns them into copyable Markdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/terminal-to-html"&gt;terminal-to-html&lt;/a&gt;&lt;/strong&gt; can optionally save the user's converted terminal session to a Gist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/bluesky-quote-finder?post=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7auwt3ma222"&gt;bluesky-quote-finder&lt;/a&gt;&lt;/strong&gt; displays quotes of a specified Bluesky post, which can then be sorted by likes or by time.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/species-observation-map#%7B%22taxonId%22%3A123829%2C%22taxonName%22%3A%22California%20Brown%20Pelican%22%2C%22days%22%3A%2230%22%7D" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/species-observation-map.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of species-observation-map" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?package=llm" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/zip-wheel-explorer.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of zip-wheel-explorer" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/github-issue-to-markdown?issue=https%3A%2F%2Fgithub.com%2Fsimonw%2Fsqlite-utils%2Fissues%2F657" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/github-issue-to-markdown.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of github-issue-to-markdown" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/terminal-to-html" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/terminal-to-html.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of terminal-to-html" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/bluesky-quote-finder?post=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7auwt3ma222" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/bluesky-quote-finder.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of bluesky-quote-finder" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="llms-can-be-called-directly-via-cors"&gt;LLMs can be called directly via CORS&lt;/h4&gt;
&lt;p&gt;All three of OpenAI, Anthropic and Gemini offer JSON APIs that can be accessed via CORS directly from HTML tools.&lt;/p&gt;
&lt;p&gt;Unfortunately you still need an API key, and if you bake that key into your visible HTML anyone can steal it and use to rack up charges on your account.&lt;/p&gt;
&lt;p&gt;I use the &lt;code&gt;localStorage&lt;/code&gt; secrets pattern to store API keys for these services. This sucks from a user experience perspective - telling users to go and create an API key and paste it into a tool is a lot of friction - but it does work.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/haiku"&gt;haiku&lt;/a&gt;&lt;/strong&gt; uses the Claude API to write a haiku about an image from the user's webcam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/openai-audio-output"&gt;openai-audio-output&lt;/a&gt;&lt;/strong&gt; generates audio speech using OpenAI's GPT-4o audio API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://tools.simonwillison.net/gemini-bbox"&gt;gemini-bbox&lt;/a&gt;&lt;/strong&gt; demonstrates Gemini 2.5's ability to return complex shaped image masks for objects in images, see &lt;a href="https://simonwillison.net/2025/Apr/18/gemini-image-segmentation/"&gt;Image segmentation using Gemini 2.5&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/haiku" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/haiku.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of haiku" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/openai-audio-output" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/openai-audio-output.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of openai-audio-output" /&gt;&lt;/a&gt;
  &lt;a href="http://tools.simonwillison.net/gemini-bbox" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/gemini-bbox.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of gemini-bbox" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="don-t-be-afraid-of-opening-files"&gt;Don't be afraid of opening files&lt;/h4&gt;
&lt;p&gt;You don't need to upload a file to a server in order to make use of the &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; element. JavaScript can access the content of that file directly, which opens up a wealth of opportunities for useful functionality.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ocr"&gt;ocr&lt;/a&gt;&lt;/strong&gt; is the first tool I built for my collection, described in &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;Running OCR against PDFs and images directly in your browser&lt;/a&gt;. It uses &lt;code&gt;PDF.js&lt;/code&gt; and &lt;code&gt;Tesseract.js&lt;/code&gt; to allow users to open a PDF in their browser which it then converts to an image-per-page and runs through OCR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/social-media-cropper"&gt;social-media-cropper&lt;/a&gt;&lt;/strong&gt; lets you open (or paste in) an existing image and then crop it to common dimensions needed for different social media platforms - 2:1 for Twitter and LinkedIn, 1.4:1 for Substack etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ffmpeg-crop"&gt;ffmpeg-crop&lt;/a&gt;&lt;/strong&gt; lets you open and preview a video file in your browser, drag a crop box within it and then copy out the &lt;code&gt;ffmpeg&lt;/code&gt; command needed to produce a cropped copy on your own machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/ocr" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ocr.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ocr" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/social-media-cropper" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/social-media-cropper.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of social-media-cropper" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/ffmpeg-crop" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ffmpeg-crop.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ffmpeg-crop" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="you-can-offer-downloadable-files-too"&gt;You can offer downloadable files too&lt;/h4&gt;
&lt;p&gt;An HTML tool can generate a file for download without needing help from a server.&lt;/p&gt;
&lt;p&gt;The JavaScript library ecosystem has a huge range of packages for generating files in all kinds of useful formats.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/svg-render"&gt;svg-render&lt;/a&gt;&lt;/strong&gt; lets the user download the PNG or JPEG rendered from an SVG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/social-media-cropper"&gt;social-media-cropper&lt;/a&gt;&lt;/strong&gt; does the same for cropped images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/open-sauce-2025"&gt;open-sauce-2025&lt;/a&gt;&lt;/strong&gt; is my alternative schedule for a conference that includes a downloadable ICS file for adding the schedule to your calendar. See &lt;a href="https://simonwillison.net/2025/Jul/17/vibe-scraping/"&gt;Vibe scraping and vibe coding a schedule app for Open Sauce 2025 entirely on my phone&lt;/a&gt; for more on that project.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/svg-render" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/svg-render.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of svg-render" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/social-media-cropper" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/social-media-cropper.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of social-media-cropper" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/open-sauce-2025" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/open-sauce-2025.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of open-sauce-2025" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="pyodide-can-run-python-code-in-the-browser"&gt;Pyodide can run Python code in the browser&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; is a distribution of Python that's compiled to WebAssembly and designed to run directly in browsers. It's an engineering marvel and one of the most underrated corners of the Python world.&lt;/p&gt;
&lt;p&gt;It also cleanly loads from a CDN, which means there's no reason not to use it in HTML tools!&lt;/p&gt;
&lt;p&gt;Even better, the Pyodide project includes &lt;a href="https://github.com/pyodide/micropip"&gt;micropip&lt;/a&gt; - a mechanism that can load extra pure-Python packages from PyPI via CORS.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pyodide-bar-chart"&gt;pyodide-bar-chart&lt;/a&gt;&lt;/strong&gt; demonstrates running Pyodide, Pandas and matplotlib to render a bar chart directly in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/numpy-pyodide-lab"&gt;numpy-pyodide-lab&lt;/a&gt;&lt;/strong&gt; is an experimental interactive tutorial for Numpy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/apsw-query"&gt;apsw-query&lt;/a&gt;&lt;/strong&gt; demonstrates the &lt;a href="https://github.com/rogerbinns/apsw"&gt;APSW SQLite library&lt;/a&gt;  running in a browser, using it to show EXPLAIN QUERY plans for SQLite queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/pyodide-bar-chart" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/pyodide-bar-chart.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of pyodide-bar-chart" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/numpy-pyodide-lab" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/numpy-pyodide-lab.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of numpy-pyodide-lab" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/apsw-query" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/apsw-query.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of apsw-query" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="webassembly-opens-more-possibilities"&gt;WebAssembly opens more possibilities&lt;/h4&gt;
&lt;p&gt;Pyodide is possible thanks to WebAssembly. WebAssembly means that a vast collection of software originally written in other languages can now be loaded in HTML tools as well.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://squoosh.app/"&gt;Squoosh.app&lt;/a&gt; was the first example I saw that convinced me of the power of this pattern - it makes several best-in-class image compression libraries available directly in the browser.&lt;/p&gt;
&lt;p&gt;I've used WebAssembly for a few of my own tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ocr"&gt;ocr&lt;/a&gt;&lt;/strong&gt; uses the pre-existing &lt;a href="https://tesseract.projectnaptha.com/"&gt;Tesseract.js&lt;/a&gt; WebAssembly port of the Tesseract OCR engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/sloccount"&gt;sloccount&lt;/a&gt;&lt;/strong&gt; is a port of David Wheeler's Perl and C &lt;a href="https://dwheeler.com/sloccount/"&gt;SLOCCount&lt;/a&gt; utility to the browser, using a big ball of WebAssembly duct tape. &lt;a href="https://simonwillison.net/2025/Oct/22/sloccount-in-webassembly/"&gt;More details here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/micropython"&gt;micropython&lt;/a&gt;&lt;/strong&gt; is my experiment using &lt;a href="https://www.npmjs.com/package/@micropython/micropython-webassembly-pyscript"&gt;@micropython/micropython-webassembly-pyscript&lt;/a&gt; from NPM to run Python code with a smaller initial download than Pyodide.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/ocr" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ocr.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ocr" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/sloccount" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/sloccount.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of sloccount" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/micropython" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/micropython.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of micropython" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="remix-your-previous-tools"&gt;Remix your previous tools&lt;/h4&gt;
&lt;p&gt;The biggest advantage of having a single public collection of 100+ tools is that it's easy for my LLM assistants to recombine them in interesting ways.&lt;/p&gt;
&lt;p&gt;Sometimes I'll copy and paste a previous tool into the context, but when I'm working with a coding agent I can reference them by name - or tell the agent to search for relevant examples before it starts work.&lt;/p&gt;
&lt;p&gt;The source code of any working tool doubles as clear documentation of how something can be done, including patterns for using editing libraries. An LLM with one or two existing tools in their context is much more likely to produce working code.&lt;/p&gt;
&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pypi-changelog"&gt;pypi-changelog&lt;/a&gt;&lt;/strong&gt; by telling Claude Code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Look at the pypi package explorer tool&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then, after it had found and read the source code for &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer"&gt;zip-wheel-explorer&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a new tool pypi-changelog.html which uses the PyPI API to get the wheel URLs of all available versions of a package, then it displays them in a list where each pair has a "Show changes" clickable in between them - clicking on that fetches the full contents of the wheels and displays a nicely rendered diff representing the difference between the two, as close to a standard diff format as you can get with JS libraries from CDNs, and when that is displayed there is a "Copy" button which copies that diff to the clipboard&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gistpreview.github.io/?9b48fd3f8b99a204ba2180af785c89d2"&gt;the full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;See &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;Running OCR against PDFs and images directly in your browser&lt;/a&gt; for another detailed example of remixing tools to create something new.&lt;/p&gt;
&lt;h4 id="record-the-prompt-and-transcript"&gt;Record the prompt and transcript&lt;/h4&gt;
&lt;p&gt;I like keeping (and publishing) records of everything I do with LLMs, to help me grow my skills at using them over time.&lt;/p&gt;
&lt;p&gt;For HTML tools I built by chatting with an LLM platform directly I use the "share" feature for those platforms.&lt;/p&gt;
&lt;p&gt;For Claude Code or Codex CLI or other coding agents I copy and paste the full transcript from the terminal into my &lt;a href="https://tools.simonwillison.net/terminal-to-html"&gt;terminal-to-html&lt;/a&gt; tool and share that using a Gist.&lt;/p&gt;
&lt;p&gt;In either case I include links to those transcripts in the commit message when I save the finished tool to my repository. You can see those &lt;a href="https://tools.simonwillison.net/colophon"&gt;in my tools.simonwillison.net colophon&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="go-forth-and-build"&gt;Go forth and build&lt;/h4&gt;
&lt;p&gt;I've had &lt;em&gt;so much fun&lt;/em&gt; exploring the capabilities of LLMs in this way over the past year and a half, and building tools in this way has been invaluable in helping me understand both the potential for building tools with HTML and the capabilities of the LLMs that I'm building them with.&lt;/p&gt;
&lt;p&gt;If you're interested in starting your own collection I highly recommend it! All you need to get started is a free GitHub repository with GitHub Pages enabled (Settings -&amp;gt; Pages -&amp;gt; Source -&amp;gt; Deploy from a branch -&amp;gt; main) and you can start copying in &lt;code&gt;.html&lt;/code&gt; pages generated in whatever manner you like.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;strong&gt;Bonus transcript&lt;/strong&gt;: Here's &lt;a href="http://gistpreview.github.io/?1b8cba6a8a21110339cbde370e755ba0"&gt;how I used Claude Code&lt;/a&gt; and &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; to add the screenshots to this post.&lt;/small&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/definitions"&gt;definitions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&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/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&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/vibe-coding"&gt;vibe-coding&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="definitions"/><category term="github"/><category term="html"/><category term="javascript"/><category term="localstorage"/><category term="projects"/><category term="tools"/><category term="ai"/><category term="webassembly"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="vibe-coding"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>The fetch()ening</title><link href="https://simonwillison.net/2025/Nov/3/htmx-the-fetchening/#atom-tag" rel="alternate"/><published>2025-11-03T21:39:54+00:00</published><updated>2025-11-03T21:39:54+00:00</updated><id>https://simonwillison.net/2025/Nov/3/htmx-the-fetchening/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://htmx.org/essays/the-fetchening/"&gt;The fetch()ening&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
After several years of stable htmx 2.0 and a promise to never release a backwards-incompatible htmx 3 Carson Gross is technically keeping that promise... by skipping to htmx 4 instead!&lt;/p&gt;
&lt;p&gt;The main reason is to replace &lt;code&gt;XMLHttpRequest&lt;/code&gt; with &lt;code&gt;fetch()&lt;/code&gt; - a change that will have enough knock-on compatibility effects to require a major version bump - so they're using that as an excuse to clean up various other accumulated design warts at the same time.&lt;/p&gt;
&lt;p&gt;htmx is a &lt;em&gt;very&lt;/em&gt; responsibly run project. Here's their plan for the upgrade:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That said, htmx 2.0 users &lt;em&gt;will&lt;/em&gt; face an upgrade project when moving to 4.0 in a way that they did not have to in moving from 1.0 to 2.0.&lt;/p&gt;
&lt;p&gt;I am sorry about that, and want to offer three things to address it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;htmx 2.0 (like htmx 1.0 &amp;amp; intercooler.js 1.0) will be supported &lt;em&gt;in perpetuity&lt;/em&gt;, so there is absolutely &lt;em&gt;no&lt;/em&gt; pressure to upgrade your application: if htmx 2.0 is satisfying your hypermedia needs, you can stick with it.&lt;/li&gt;
&lt;li&gt;We will create extensions that revert htmx 4 to htmx 2 behaviors as much as is feasible (e.g. Supporting the old implicit attribute inheritance model, at least)&lt;/li&gt;
&lt;li&gt;We will roll htmx 4.0 out slowly, over a multi-year period. As with the htmx 1.0 -&amp;gt; 2.0 upgrade, there will be a long period where htmx 2.x is &lt;code&gt;latest&lt;/code&gt; and htmx 4.x is &lt;code&gt;next&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are lots of neat details in here about the design changes they plan to make. It's a really great piece of technical writing - I learned a bunch about htmx and picked up some good notes on API design in general from this.

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


    &lt;p&gt;Tags: &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/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/htmx"&gt;htmx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/carson-gross"&gt;carson-gross&lt;/a&gt;&lt;/p&gt;



</summary><category term="html"/><category term="javascript"/><category term="open-source"/><category term="htmx"/><category term="carson-gross"/></entry><entry><title>Should form labels be wrapped or separate?</title><link href="https://simonwillison.net/2025/Oct/17/form-labels/#atom-tag" rel="alternate"/><published>2025-10-17T18:25:45+00:00</published><updated>2025-10-17T18:25:45+00:00</updated><id>https://simonwillison.net/2025/Oct/17/form-labels/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.tpgi.com/should-form-labels-be-wrapped-or-separate/"&gt;Should form labels be wrapped or separate?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
James Edwards notes that wrapping a form input in a label event like this has a significant downside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label&amp;gt;Name &amp;lt;input type="text"&amp;gt;&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It turns out both Dragon Naturally Speaking for Windows and Voice Control for macOS and iOS fail to understand this relationship!&lt;/p&gt;
&lt;p&gt;You need to use the explicit &lt;code&gt;&amp;lt;label for="element_id"&amp;gt;&lt;/code&gt; syntax to ensure those screen readers correctly understand the relationship between label and form field. You can still nest the input inside the label if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for="idField"&amp;gt;Name
  &amp;lt;input id="idField" type="text"&amp;gt;
&amp;lt;/label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://gomakethings.com/implicit-labels-arent/"&gt;Chris Ferdinandi&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="accessibility"/><category term="html"/><category term="screen-readers"/></entry><entry><title>XSLT on congress.gov</title><link href="https://simonwillison.net/2025/Aug/19/xslt/#atom-tag" rel="alternate"/><published>2025-08-19T20:40:50+00:00</published><updated>2025-08-19T20:40:50+00:00</updated><id>https://simonwillison.net/2025/Aug/19/xslt/#atom-tag</id><summary type="html">
    &lt;p&gt;Today I learned - via &lt;a href="https://github.com/whatwg/html/pull/11563"&gt;a proposal to remove mentions of XSLT from the HTML spec&lt;/a&gt; - that &lt;code&gt;congress.gov&lt;/code&gt; uses XSLT to serve XML bills as XHTML - here's &lt;a href="https://www.congress.gov/117/bills/hr3617/BILLS-117hr3617ih.xml"&gt;H. R. 3617 117th CONGRESS 1st Session&lt;/a&gt; for example.&lt;/p&gt;
&lt;p&gt;View source on that page and it starts like this:&lt;/p&gt;
&lt;pre&gt;&amp;lt;?&lt;span class="pl-ent"&gt;xml&lt;/span&gt;&lt;span class="pl-e"&gt; version&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1.0&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;?&amp;gt;
&amp;lt;?&lt;span class="pl-ent"&gt;xml-stylesheet&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;text/xsl&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-e"&gt; href&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;billres.xsl&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;?&amp;gt;
&amp;lt;!&lt;span class="pl-ent"&gt;DOCTYPE&lt;/span&gt; &lt;span class="pl-e"&gt;bill&lt;/span&gt; PUBLIC "-//US Congress//DTDs/bill.dtd//EN" "bill.dtd"&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;bill&lt;/span&gt; &lt;span class="pl-e"&gt;bill-stage&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Introduced-in-House&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;dms-id&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;H5BD50AB7712141319B352D46135AAC2B&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;public-private&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;public&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;key&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;H&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-e"&gt;bill-type&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;olc&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt; 
&amp;lt;&lt;span class="pl-ent"&gt;metadata&lt;/span&gt; &lt;span class="pl-e"&gt;xmlns&lt;/span&gt;&lt;span class="pl-e"&gt;:&lt;/span&gt;&lt;span class="pl-e"&gt;dc&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;http://purl.org/dc/elements/1.1/&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dublinCore&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&amp;gt;117 HR 3617 IH: Marijuana Opportunity Reinvestment and Expungement Act of 2021&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;publisher&lt;/span&gt;&amp;gt;U.S. House of Representatives&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;publisher&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;date&lt;/span&gt;&amp;gt;2021-05-28&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;date&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;format&lt;/span&gt;&amp;gt;text/xml&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;format&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;language&lt;/span&gt;&amp;gt;EN&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;language&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;rights&lt;/span&gt;&amp;gt;Pursuant to Title 17 Section 105 of the United States Code, this file is not subject to copyright protection and is in the public domain.&amp;lt;/&lt;span class="pl-ent"&gt;dc&lt;/span&gt;&lt;span class="pl-ent"&gt;:&lt;/span&gt;&lt;span class="pl-ent"&gt;rights&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;dublinCore&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;metadata&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;form&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;distribution-code&lt;/span&gt; &lt;span class="pl-e"&gt;display&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;yes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;I&amp;lt;/&lt;span class="pl-ent"&gt;distribution-code&lt;/span&gt;&amp;gt; 
&amp;lt;&lt;span class="pl-ent"&gt;congress&lt;/span&gt; &lt;span class="pl-e"&gt;display&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;yes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;117th CONGRESS&amp;lt;/&lt;span class="pl-ent"&gt;congress&lt;/span&gt;&amp;gt;&amp;lt;&lt;span class="pl-ent"&gt;session&lt;/span&gt; &lt;span class="pl-e"&gt;display&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;yes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;1st Session&amp;lt;/&lt;span class="pl-ent"&gt;session&lt;/span&gt;&amp;gt; 
&amp;lt;&lt;span class="pl-ent"&gt;legis-num&lt;/span&gt; &lt;span class="pl-e"&gt;display&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;yes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;H. R. 3617&amp;lt;/&lt;span class="pl-ent"&gt;legis-num&lt;/span&gt;&amp;gt; 
&amp;lt;&lt;span class="pl-ent"&gt;current-chamber&lt;/span&gt;&amp;gt;IN THE HOUSE OF REPRESENTATIVES&amp;lt;/&lt;span class="pl-ent"&gt;current-chamber&lt;/span&gt;&amp;gt;&lt;/pre&gt;

&lt;p&gt;Digging into those XSLT stylesheets leads to &lt;code&gt;billres-details.xsl&lt;/code&gt; - &lt;a href="https://gist.github.com/simonw/64c9f172533203c09acbcf13a0bb67c4"&gt;gist copy here&lt;/a&gt; - which starts with a huge changelog comment with notes dating all the way back to 2004!&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/xslt"&gt;xslt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/political-hacking"&gt;political-hacking&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;/p&gt;



</summary><category term="xslt"/><category term="political-hacking"/><category term="web-standards"/><category term="html"/></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/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;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="javascript"/><category term="web-development"/><category term="frontend"/><category term="html"/></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/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/minecraft"&gt;minecraft&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="html"/><category term="minecraft"/></entry><entry><title>Giving software away for free</title><link href="https://simonwillison.net/2025/Apr/28/give-it-away-for-free/#atom-tag" rel="alternate"/><published>2025-04-28T16:10:58+00:00</published><updated>2025-04-28T16:10:58+00:00</updated><id>https://simonwillison.net/2025/Apr/28/give-it-away-for-free/#atom-tag</id><summary type="html">
    &lt;p&gt;If you want to create completely &lt;strong&gt;free software&lt;/strong&gt; for other people to use, the absolute best delivery mechanism right now is static HTML and JavaScript served from a free web host with an established reputation.&lt;/p&gt;
&lt;p&gt;Thanks to WebAssembly the set of potential software that can be served in this way is vast and, I think, under appreciated. &lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; means we can ship client-side Python applications now!&lt;/p&gt;
&lt;p&gt;This assumes that you would like your gift to the world to keep working for as long as possible, while granting you the freedom to lose interest and move onto other projects without needing to keep covering expenses far into the future.&lt;/p&gt;
&lt;p&gt;Even the cheapest hosting plan requires you to monitor and update billing details every few years. Domains have to be renewed. Anything that runs server-side will inevitably need to be upgraded someday - and the longer you wait between upgrades the harder those become.&lt;/p&gt;
&lt;p&gt;My top choice for this kind of thing in 2025 is GitHub, using GitHub Pages. It's free for public repositories and I haven't seen GitHub break a working URL that they have hosted in the 17+ years since they first launched.&lt;/p&gt;
&lt;p&gt;A few years ago I'd have recommended Heroku on the basis that their free plan had stayed reliable for more than a decade, but Salesforce took that accumulated goodwill and &lt;a href="https://blog.heroku.com/next-chapter"&gt;incinerated it in 2022&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It almost goes without saying that you should release it under an open source license. The license alone is not enough to ensure regular human beings can make use of what you have built though: give people a link to something that works!&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&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;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/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="heroku"/><category term="webassembly"/><category term="javascript"/><category term="web-standards"/><category term="html"/><category term="github"/><category term="pyodide"/></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/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/safari"&gt;safari&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="chrome"/><category term="css"/><category term="firefox"/><category term="html"/><category term="mozilla"/><category term="safari"/><category term="web-standards"/></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/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/css"&gt;css&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;&lt;/p&gt;



</summary><category term="chrome"/><category term="css"/><category term="firefox"/><category term="html"/><category term="web-standards"/></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/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;a href="https://simonwillison.net/tags/til"&gt;til&lt;/a&gt;&lt;/p&gt;



</summary><category term="css"/><category term="html"/><category term="natalie-downe"/><category term="web-standards"/><category term="til"/></entry><entry><title>strip-tags 0.6</title><link href="https://simonwillison.net/2025/Feb/28/strip-tags/#atom-tag" rel="alternate"/><published>2025-02-28T22:02:16+00:00</published><updated>2025-02-28T22:02:16+00:00</updated><id>https://simonwillison.net/2025/Feb/28/strip-tags/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/strip-tags/releases/tag/0.6"&gt;strip-tags 0.6&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It's been a while since I updated this tool, but in investigating &lt;a href="https://github.com/simonw/llm/issues/808"&gt;a tricky mistake&lt;/a&gt; in my tutorial for LLM schemas I discovered &lt;a href="https://github.com/simonw/strip-tags/issues/32"&gt;a bug&lt;/a&gt; that I needed to fix.&lt;/p&gt;
&lt;p&gt;Those release notes in full:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Fixed a bug where &lt;code&gt;strip-tags -t meta&lt;/code&gt; still removed &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags from the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; because the entire &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; element was removed first. &lt;a href="https://github.com/simonw/strip-tags/issues/32"&gt;#32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Kept &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags now default to keeping their &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;property&lt;/code&gt; attributes.&lt;/li&gt;
&lt;li&gt;The CLI &lt;code&gt;-m/--minify&lt;/code&gt; option now also removes any remaining blank lines. &lt;a href="https://github.com/simonw/strip-tags/issues/33"&gt;#33&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A new &lt;code&gt;strip_tags(remove_blank_lines=True)&lt;/code&gt; option can be used to achieve the same thing with the Python library function.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now I can do this and persist the &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags for the article along with the stripped text content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s 'https://apnews.com/article/trump-federal-employees-firings-a85d1aaf1088e050d39dcf7e3664bb9f' | \
  strip-tags -t meta --minify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/22902a75e2e73ca513231e1d8d0dac6e"&gt;the output from that command&lt;/a&gt;.


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



</summary><category term="cli"/><category term="html"/><category term="projects"/></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/accessibility"&gt;accessibility&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &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/webassembly"&gt;webassembly&lt;/a&gt;&lt;/p&gt;



</summary><category term="accessibility"/><category term="c"/><category term="css"/><category term="html"/><category term="webassembly"/></entry><entry><title>Using static websites for tiny archives</title><link href="https://simonwillison.net/2024/Oct/17/using-static-websites-for-tiny-archives/#atom-tag" rel="alternate"/><published>2024-10-17T23:02:18+00:00</published><updated>2024-10-17T23:02:18+00:00</updated><id>https://simonwillison.net/2024/Oct/17/using-static-websites-for-tiny-archives/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://alexwlchan.net/2024/static-websites/"&gt;Using static websites for tiny archives&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Alex Chan:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Over the last year or so, I’ve been creating static websites to browse my local archives. I’ve done this for a variety of collections, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;paperwork I’ve scanned&lt;/li&gt;
&lt;li&gt;documents I’ve created&lt;/li&gt;
&lt;li&gt;screenshots I’ve taken&lt;/li&gt;
&lt;li&gt;web pages I’ve bookmarked&lt;/li&gt;
&lt;li&gt;video and audio files I’ve saved&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is &lt;em&gt;such&lt;/em&gt; a neat idea. These tiny little personal archive websites aren't even served through a localhost web server - they exist as folders on disk, and Alex browses them by opening up the &lt;code&gt;index.html&lt;/code&gt; file directly in a browser.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://social.alexwlchan.net/@alex/113318585934019063"&gt;@alex&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="archives"/><category term="html"/></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>Reckoning</title><link href="https://simonwillison.net/2024/Aug/18/reckoning/#atom-tag" rel="alternate"/><published>2024-08-18T16:37:41+00:00</published><updated>2024-08-18T16:37:41+00:00</updated><id>https://simonwillison.net/2024/Aug/18/reckoning/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://infrequently.org/series/reckoning/"&gt;Reckoning&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Alex Russell is a self-confessed &lt;a href="https://en.wikipedia.org/wiki/Cassandra"&gt;Cassandra&lt;/a&gt; - doomed to speak truth that the wider Web industry stubbornly ignores. With this latest series of posts he is &lt;em&gt;spitting fire&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The series is an "investigation into JavaScript-first frontend culture and how it broke US public services", in four parts.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://infrequently.org/2024/08/object-lesson/"&gt;Part 2 — Object Lesson&lt;/a&gt; Alex profiles &lt;a href="https://benefitscal.com/"&gt;BenefitsCal&lt;/a&gt;, the California state portal for accessing SNAP food benefits (aka "food stamps"). On a 9Mbps connection, as can be expected in rural parts of California with populations most likely to need these services, the site takes 29.5 seconds to become usefully interactive, fetching more than 20MB of JavaScript (which isn't even correctly compressed) for a giant SPA that incoroprates React, Vue, the AWS JavaScript SDK, six user-agent parsing libraries and &lt;a href="https://infrequently.org/2024/08/object-lesson/#fn-receipts-1"&gt;a whole lot more&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It doesn't have to be like this! &lt;a href="https://www.getcalfresh.org/"&gt;GetCalFresh.org&lt;/a&gt;, the Code for America alternative to BenefitsCal, becomes interactive after 4 seconds. Despite not being the "official" site it has driven nearly half of all signups for California benefits.&lt;/p&gt;
&lt;p&gt;The fundamental problem here is the Web industry's obsession with SPAs and JavaScript-first development - techniques that make sense for a tiny fraction of applications (Alex &lt;a href="https://infrequently.org/2024/08/caprock/"&gt;calls out&lt;/a&gt; document editors, chat and videoconferencing and maps, geospatial, and BI visualisations as apppropriate applications) but massively increase the cost and complexity for the vast majority of sites - especially sites primarily used on mobile and that shouldn't expect lengthy session times or multiple repeat visits.&lt;/p&gt;
&lt;p&gt;There's so much great, quotable content in here. Don't miss out on the footnotes, like &lt;a href="https://infrequently.org/2024/08/caprock/#fn-omerta-as-market-failure-3"&gt;this one&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The JavaScript community's omertà regarding the consistent failure of frontend frameworks to deliver reasonable results at acceptable cost is likely to be remembered as one of the most shameful aspects of frontend's lost decade.&lt;/p&gt;
&lt;p&gt;Had the risks been prominently signposted, dozens of teams I've worked with personally could have avoided months of painful remediation, and hundreds more sites I've traced could have avoided material revenue losses.&lt;/p&gt;
&lt;p&gt;Too many engineering leaders have found their teams beached and unproductive for no reason other than the JavaScript community's dedication to a marketing-over-results ethos of toxic positivity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In &lt;a href="https://infrequently.org/2024/08/the-way-out/"&gt;Part 4 — The Way Out&lt;/a&gt; Alex recommends the &lt;a href="https://www.gov.uk/service-manual"&gt;gov.uk Service Manual&lt;/a&gt; as a guide for building civic Web services that avoid these traps, thanks to the policy described in their &lt;a href="https://www.gov.uk/service-manual/technology/using-progressive-enhancement"&gt;Building a resilient frontend using progressive enhancement&lt;/a&gt; document.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/accessibility"&gt;accessibility&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-russell"&gt;alex-russell&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/government"&gt;government&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/progressive-enhancement"&gt;progressive-enhancement&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-performance"&gt;web-performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gov-uk"&gt;gov-uk&lt;/a&gt;&lt;/p&gt;



</summary><category term="accessibility"/><category term="alex-russell"/><category term="government"/><category term="html"/><category term="javascript"/><category term="progressive-enhancement"/><category term="web-performance"/><category term="gov-uk"/></entry><entry><title>This month in Servo: parallel tables and more</title><link href="https://simonwillison.net/2024/Jul/31/servo-parallel-tables/#atom-tag" rel="alternate"/><published>2024-07-31T15:03:28+00:00</published><updated>2024-07-31T15:03:28+00:00</updated><id>https://simonwillison.net/2024/Jul/31/servo-parallel-tables/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://servo.org/blog/2024/07/31/this-month-in-servo/"&gt;This month in Servo: parallel tables and more&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New in Servo:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Parallel table layout&lt;/strong&gt; is now enabled (&lt;a href="https://github.com/mrobinson"&gt;@mrobinson&lt;/a&gt;, &lt;a href="https://github.com/servo/servo/pull/32477"&gt;#32477&lt;/a&gt;), spreading the work for laying out rows and their columns over all available CPU cores. This change is a great example of the strengths of &lt;a href="https://crates.io/crates/rayon"&gt;Rayon&lt;/a&gt; and the opportunistic parallelism in Servo's layout engine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href="https://github.com/servo/servo/commit/e16291f14edc38d4bc3663a36619e6e461329402?diff=unified&amp;amp;w=0"&gt;commit landing the change&lt;/a&gt; is quite short, and much of the work is done by refactoring the code to use &lt;code&gt;.par_iter().enumerate().map(...)&lt;/code&gt; - &lt;a href="https://docs.rs/rayon/latest/rayon/iter/index.html"&gt;par_iter()&lt;/a&gt; is the Rayon method that allows parallel iteration over a collection using multiple threads, hence multiple CPU cores.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://www.phoronix.com/news/Servo-Multi-Core-HTML-Tables"&gt;Phoronix&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/concurrency"&gt;concurrency&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/servo"&gt;servo&lt;/a&gt;&lt;/p&gt;



</summary><category term="concurrency"/><category term="html"/><category term="rust"/><category term="servo"/></entry><entry><title>Quoting Andreas Kling</title><link href="https://simonwillison.net/2024/Jun/23/andreas-kling/#atom-tag" rel="alternate"/><published>2024-06-23T23:59:15+00:00</published><updated>2024-06-23T23:59:15+00:00</updated><id>https://simonwillison.net/2024/Jun/23/andreas-kling/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://twitter.com/awesomekling/status/1803412879816659243"&gt;&lt;p&gt;For some reason, many people still believe that browsers need to include non-standard hacks in HTML parsing to display the web correctly.&lt;/p&gt;
&lt;p&gt;In reality, the HTML parsing spec is exhaustively detailed. If you implement it as described, you will have a web-compatible parser.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://twitter.com/awesomekling/status/1803412879816659243"&gt;Andreas Kling&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&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/andreas-kling"&gt;andreas-kling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ladybird"&gt;ladybird&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="html"/><category term="web-standards"/><category term="andreas-kling"/><category term="ladybird"/></entry><entry><title>Streaming HTML out of order without JavaScript</title><link href="https://simonwillison.net/2024/Mar/1/streaming-html-out-of-order-without-javascript/#atom-tag" rel="alternate"/><published>2024-03-01T16:59:54+00:00</published><updated>2024-03-01T16:59:54+00:00</updated><id>https://simonwillison.net/2024/Mar/1/streaming-html-out-of-order-without-javascript/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lamplightdev.com/blog/2024/01/10/streaming-html-out-of-order-without-javascript/"&gt;Streaming HTML out of order without JavaScript&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A really interesting new browser capability. If you serve the following HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template shadowrootmode="open"&amp;gt;
  &amp;lt;slot name="item-1"&amp;gt;Loading...&amp;lt;/slot&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then later in the same page stream an element specifying that slot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span slot="item-1"&amp;gt;Item number 1&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The previous slot will be replaced while the page continues to load.&lt;/p&gt;
&lt;p&gt;I tried the demo in the most recent Chrome, Safari and Firefox (and Mobile Safari) and it worked in all of them.&lt;/p&gt;
&lt;p&gt;The key feature is &lt;code&gt;shadowrootmode=open&lt;/code&gt;, which looks like it was added to Firefox 123 on February 19th 2024 - the other two browsers are listed on caniuse.com as gaining it around March last year.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&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="browsers"/><category term="html"/><category term="web-components"/></entry><entry><title>htmz</title><link href="https://simonwillison.net/2024/Feb/20/htmz/#atom-tag" rel="alternate"/><published>2024-02-20T01:21:24+00:00</published><updated>2024-02-20T01:21:24+00:00</updated><id>https://simonwillison.net/2024/Feb/20/htmz/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://leanrada.com/htmz/"&gt;htmz&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Astonishingly clever browser platform hack by Lean Rada.&lt;/p&gt;
&lt;p&gt;Add this to a page:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;iframe hidden name=htmz onload="setTimeout(() =&amp;gt; document.querySelector( this.contentWindow.location.hash || null)?.replaceWith( ...this.contentDocument.body.childNodes ))"&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then elsewhere add a link like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;a href="/flower.html#my-element" target=htmz&amp;gt;Flower&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Clicking that link will fetch content from &lt;code&gt;/flower.html&lt;/code&gt; and replace the element with ID of &lt;code&gt;my-element&lt;/code&gt; with that content.

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


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



</summary><category term="html"/><category term="iframes"/><category term="javascript"/></entry><entry><title>Portable EPUBs</title><link href="https://simonwillison.net/2024/Jan/25/portable-epubs/#atom-tag" rel="alternate"/><published>2024-01-25T20:32:38+00:00</published><updated>2024-01-25T20:32:38+00:00</updated><id>https://simonwillison.net/2024/Jan/25/portable-epubs/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://willcrichton.net/notes/portable-epubs/"&gt;Portable EPUBs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Will Crichton digs into the reasons people still prefer PDF over HTML as a format for sharing digital documents, concluding that the key issues are that HTML documents are not fully self-contained and may not be rendered consistently.&lt;/p&gt;

&lt;p&gt;He proposes “Portable EPUBs” as the solution, defining a subset of the existing EPUB standard with some additional restrictions around avoiding loading extra assets over a network, sticking to a smaller (as-yet undefined) subset of HTML and encouraging interactive components to be built using self-contained Web Components.&lt;/p&gt;

&lt;p&gt;Will also built his own lightweight EPUB reading system, called Bene—which is used to render this Portable EPUBs article. It provides a “download” link in the top right which produces the .epub file itself.&lt;/p&gt;

&lt;p&gt;There’s a lot to like here. I’m constantly infuriated at the number of documents out there that are PDFs but really should be web pages (academic papers are a particularly bad example here), so I’m very excited by any initiatives that might help push things in the other direction.


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



</summary><category term="html"/><category term="pdf"/><category term="web-components"/></entry><entry><title>You can stop using user-scalable=no and maximum-scale=1 in viewport meta tags now</title><link href="https://simonwillison.net/2023/Aug/4/you-can-stop-using-user-scalableno-and-maximum-scale1-in-viewpor/#atom-tag" rel="alternate"/><published>2023-08-04T23:41:55+00:00</published><updated>2023-08-04T23:41:55+00:00</updated><id>https://simonwillison.net/2023/Aug/4/you-can-stop-using-user-scalableno-and-maximum-scale1-in-viewpor/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lukeplant.me.uk/blog/posts/you-can-stop-using-user-scalable-no-and-maximum-scale-1-in-viewport-meta-tags-now/"&gt;You can stop using user-scalable=no and maximum-scale=1 in viewport meta tags now&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Luke Plant points out that your meta viewport tag should stick to just “width=device-width, initial-scale=1” these days—the user-scalable=no and maximum-scale=1 attributes are no longer necessary, and have a negative impact on accessibility, especially for Android users.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/accessibility"&gt;accessibility&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/luke-plant"&gt;luke-plant&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mobile"&gt;mobile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mobileweb"&gt;mobileweb&lt;/a&gt;&lt;/p&gt;



</summary><category term="accessibility"/><category term="html"/><category term="luke-plant"/><category term="mobile"/><category term="mobileweb"/></entry><entry><title>The Page With No Code</title><link href="https://simonwillison.net/2023/Jan/21/the-page-with-no-code/#atom-tag" rel="alternate"/><published>2023-01-21T18:59:56+00:00</published><updated>2023-01-21T18:59:56+00:00</updated><id>https://simonwillison.net/2023/Jan/21/the-page-with-no-code/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://danq.me/2023/01/11/nocode/"&gt;The Page With No Code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A fun demo by Dan Q, who created a web page with no HTML at all - but in Firefox it still renders content, thanks to a data URI base64 encoded stylesheet served in a &lt;code&gt;link:&lt;/code&gt; header that uses &lt;code&gt;html::before&lt;/code&gt;, &lt;code&gt;html::after&lt;/code&gt;, &lt;code&gt;body::before&lt;/code&gt; and &lt;code&gt;body::after&lt;/code&gt; with &lt;code&gt;content:&lt;/code&gt; properties to serve the content. It even has a background image, encoded as a base64 SVG nested inside another data URI.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://gist.github.com/simonw/1fc19bf4afceb5192d7670ee05162fab"&gt;I decoded it in this Gist&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/datauri"&gt;datauri&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="datauri"/><category term="html"/></entry><entry><title>Introducing sqlite-html: query, parse, and generate HTML in SQLite</title><link href="https://simonwillison.net/2022/Aug/3/sqlite-html/#atom-tag" rel="alternate"/><published>2022-08-03T17:31:16+00:00</published><updated>2022-08-03T17:31:16+00:00</updated><id>https://simonwillison.net/2022/Aug/3/sqlite-html/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@asg017/introducing-sqlite-html"&gt;Introducing sqlite-html: query, parse, and generate HTML in SQLite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another brilliant SQLite extension module from Alex Garcia, this time written in Go. sqlite-html adds a whole family of functions to SQLite for parsing and constructing HTML strings, built on the Go goquery and cascadia libraries. Once again, Alex uses an Observable notebook to describe the new features, with embedded interactive examples that are backed by a Datasette instance running in Fly.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://til.simonwillison.net/sqlite/trying-macos-extensions"&gt;My TIL on Trying out SQLite extensions on macOS&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;&lt;/p&gt;



</summary><category term="go"/><category term="html"/><category term="sqlite"/><category term="datasette"/><category term="alex-garcia"/></entry><entry><title>Fastest way to turn HTML into text in Python</title><link href="https://simonwillison.net/2022/Jul/27/fastest-way-to-turn-html-into-text-in-python/#atom-tag" rel="alternate"/><published>2022-07-27T17:55:23+00:00</published><updated>2022-07-27T17:55:23+00:00</updated><id>https://simonwillison.net/2022/Jul/27/fastest-way-to-turn-html-into-text-in-python/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.peterbe.com/plog/selectolax-or-pyquery"&gt;Fastest way to turn HTML into text in Python&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A light benchmark of the new-to-me selectolax Python library shows it performing extremely well for tasks such as extracting just the text from an HTML string, after first manipulating the DOM. selectolax is a Python binding over the Modest and Lexbor HTML parsing engines, which are written in no-outside-dependency C.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/readthedocs/readthedocs.org/blob/142e6b6aa852c1a10b8f28785828156c52ad16df/readthedocs/embed/v3/views.py"&gt;Found selectolax in readthedocs/embed/v3/views.py&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="html"/><category term="python"/></entry><entry><title>HTML event handler attributes: down the rabbit hole</title><link href="https://simonwillison.net/2022/Apr/26/html-event-handler-attributes/#atom-tag" rel="alternate"/><published>2022-04-26T20:35:08+00:00</published><updated>2022-04-26T20:35:08+00:00</updated><id>https://simonwillison.net/2022/Apr/26/html-event-handler-attributes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datastation.multiprocess.io/blog/2022-04-26-event-handler-attributes.html"&gt;HTML event handler attributes: down the rabbit hole&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;code&gt;onclick="myfunction(event)"&lt;/code&gt; is an idiom for passing the click event to a function - but how does it work? It turns out the answer is buried deep in the HTML spec - the browser wraps that string of code in a &lt;code&gt;function(event) { ... that string ... }&lt;/code&gt; function and makes the event available to its local scope that way.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/dom-scripting"&gt;dom-scripting&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;/p&gt;



</summary><category term="dom-scripting"/><category term="html"/><category term="javascript"/></entry></feed>