[HN Gopher] Useful patterns for building HTML tools
___________________________________________________________________
Useful patterns for building HTML tools
Author : simonw
Score : 212 points
Date : 2025-12-10 21:08 UTC (3 days ago)
(HTM) web link (simonwillison.net)
(TXT) w3m dump (simonwillison.net)
| wiseowise wrote:
| Amazing article with lots of useful info. Big kudos, Simon.
|
| This really showcases the power of the single page apps and why
| web will be always ahead of native for this kind of Swiss Army
| Knife tools.
|
| With LLMs, it gets ridiculously easy to "develop" (generate)
| those too.
| simonw wrote:
| It really is wild how quickly these things pop out - this one
| here took a couple of prompts, probably ten minutes from idea
| to shipping a working implementation:
| https://tools.simonwillison.net/pypi-changelog?package=llm&c...
| TomasBM wrote:
| Pretty nice. I've been using LLMs to generate different Python
| and JS tools for wrangling data for ontology engineering
| purposes.
|
| More recently, I've found a lot of benefit from using the
| _extended thinking_ mode in GPT-5 and -5.1. It tends to provide a
| fully functional and complete result from a zero-shot prompt. It
| 's as close as I've gotten to pair programming with a
| (significantly) more experienced coder.
|
| One functional example of that (with 30-50% of my own coding,
| reprompting and reviews) is my OntoGSN [1] research prototype.
| After a couple of weeks of work, it can handle different
| integration, reasoning and extension needs of people working in
| assurance, at least based on how I understood them. It's an
| example of a human-AI collab that I'm particularly proud of.
|
| [1] Playground at w3id.org/OntoGSN/
| NotMichaelBay wrote:
| Nice, I do this often enough that I created a bookmarklet to
| download an HTML file from clipboard after copying ChatGPT's code
| block.
|
| I've also been using LLMs to create and maintain a "work assist"
| Chrome extension that I load unpacked from a local directory.
| Whenever I notice a minor pain point, I get the LLM to quickly
| implement a remedy. For example, I usually have several browser
| tabs open for Jira, and they all have the same company logo as
| the favicon, so my Chrome extension changes the favicon to be the
| issue type icon (e.g. Bug, Story, etc) when the page loads. It
| saves a little time when I'm looking for a specific ticket I've
| already opened.
| lewisjoe wrote:
| I also find that sticking to a single file makes coding agents
| perform better (fewer surgical edits, faster outputs, sensible
| changes, etc).
|
| Not sure why, but the moment the file is split into files and
| subfolders, coding agents tend to do a lot more changes that what
| is absolutely necessary. That way a single html file wins!
| dave1010uk wrote:
| Thanks Simon!
|
| My tool collection [0] is inspired by yours, with a handful of
| differences. I'm only at 53 tools at the moment.
|
| What I did differently:
|
| Hosted on Cloudflare Pages. This gives you preview URLs for pull
| requests out the box. This might be possible with Github Pages
| but I haven't checked. I've used Vercel for similar projects in
| the past. Cloudflare seems to have the odd failed build that
| needs a kick from their dashboard.
|
| Some tools can make use of Workers/Functions for backend
| processing and secrets. I try to keep these to a minimum but
| they're occasionally useful.
|
| I have an AGENTS.md that's updated with a Github action to
| automatically pull in Claude-style Skills from the .skills
| directory. I blogged about this pattern and am still waiting for
| a standard to evolve [2].
|
| I have a base stylesheet that I instruct agents to pull in. This
| gives a bit of consistency and also let's them use Tailwind,
| which they'd seem to love.
|
| [0] https://tools.dave.engineer/
|
| [1] https://github.com/dave1010/tools/tree/main/functions
|
| [2] https://dave.engineer/blog/2025/11/skills-to-agents/
| TheTaytay wrote:
| Thanks for showing this! It's cool, and I enjoyed reading
| through some of the code. Note that I tried to use some of the
| regex tools that needed LLMs and got a rate limit error.
| nels wrote:
| Really enjoyed this, interesting read as always! It reminded me
| of Google Labs' recent GenTabs project [1], and also of a recent
| ACM paper on user-assembled LLM-mediated tools from web content
| [2]. Feels like similar concepts are emerging in multiple places,
| all centered around lightweight intent-driven tools rather than
| traditional apps, which I think makes a lot of sense. Curious how
| this will evolve!
|
| [1] https://labs.google/disco
|
| [2] https://dl.acm.org/doi/10.1145/3706598.3713285
| btbuildem wrote:
| I think UIs will become more "generative" or "on demand". Not
| necessarily always generated anew, but assembled from pre-
| generated (reproducible) components, to suit a specific
| workflow.
|
| I think especially in context of software that is complex and
| takes a long time to master, this could be the next
| breakthrough. Instead of paths-to-goal being buried in
| sequences of menus and config panels, workflow pathways would
| be invocable with plain language.
| girvo wrote:
| A lot of investment at some very large companies you know are
| trying to achieve this right now, and betting big on it. So
| far the lack of determinism when it _is_ required gets in the
| way, but we'll see: it feels surmountable.
| Havoc wrote:
| Great timing - I've taken up vibecoding and picked personal tools
| with simple HTML/CSS/JS/python stack as the learning ground too.
|
| Personal tools seem like a reasonable place for happy path
| vibecoding given small blast radius and LLMs can do that sort of
| static page in front of python backend really well.
|
| I've also been surprised how much active learning I'm doing
| despite specifically _not_ look at code. Between the need to spec
| things out carefully (plan.md) and fast iteration loop it 's been
| a huge boost. Having the LLM look at a plan.md and suggest
| improvements has lead to a lot of "oh I didn't think about that"
| learning on architecture and user requirements link.
|
| Presumably much of that learning boost is because I'm a hobbyist
| tier programmer, guessing professionals wouldn't experience the
| same since they learned this via manual coding trial & error over
| years.
| girvo wrote:
| > Presumably much of that learning boost is because I'm a
| hobbyist tier programmer, guessing professionals wouldn't
| experience the same since they learned this via manual coding
| trial & error over years.
|
| I can only speak for myself and my not-quite two decades of
| professional experience, but yes pretty much!
|
| It's neat to see that sped up for others though with lower
| stakes, though it's not _quite_ the same unless you prompt your
| agent to question you back a lot (Claude is much better at this
| in my experience)
| al_borland wrote:
| I've been making stuff like this for a while (pre-LLM days). They
| are great little side projects that tend to be useful without
| getting overly complex. While I've vibe coded a couple just to
| get something done that I needed, I tend to like to write them
| myself still, as I enjoy the process.
| mattkdev wrote:
| Been writing code since the 80's on my C64 and I love it but at
| this point, I will never write another line of code yet I will
| produce more code than I ever have! It's just as fun if not
| more.
| toastal wrote:
| > 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.
|
| No. You can vendor these scripts & host them 1st party so you
| aren't leaking data to these CDNs or risk users not actually
| getting the scripts. It isn't like CDNs give you a performance
| boost anymore.
|
| https://httptoolkit.com/blog/public-cdn-risks/
| chrisweekly wrote:
| Great link.
| simonw wrote:
| I wish vendoring was less of a hassle.
|
| I'll vendor and self-host for my professional projects, but for
| these small experimental utilities I've stopped caring.
| toastal wrote:
| > small experimental utilities
|
| This is what CDNs should be used for at this time--or for
| fetching the scripts to vendor. That's fine, but
| _recommending_ I don't think is the best call since one
| folk's experimental utility will inevitably get released into
| production--often not even at fault of the utility's maker.
| When I use CDNs like this, there are <!-- WARNING ... -->
| around the code just in case someone were to run with it,
| along with adding the integrity attribute.
| xnx wrote:
| If HTML tools could make network calls (CORS be damned), they
| could replace a huge portion of hosted apps.
| simonw wrote:
| I'm tempted to run a CORS proxy somewhere - maybe on Cloudflare
| Workers - but I'm nervous that any open proxy is liable to be
| abused.
|
| I could do an authentication protected one that only I could
| access though...
| born-jre wrote:
| I am building platform which makes it easy to host this kind of
| app but it has own set of idiosyncrasy u have to adhere to.
|
| https://github.com/blue-monads/potatoverse
| btbuildem wrote:
| I really like the simplicity here (maybe because it mirrors my
| own approach). No build chain, no node_packages, no frameworks.
|
| I wonder if packaging the results as web components would be the
| next logical step.
| valbaca wrote:
| HTML tools are a good name. I called them something like "a local
| html file"
|
| One problem I solved with this was a packer needed to scan a few
| (10-40) ids into his barcode scanner. It was not enough where
| pulling up their bulk-id-uploader program but also too tedious to
| go to some "number to barcode" website.
|
| Turns out, barcodes can be made from a google font!
|
| https://fonts.google.com/specimen/Libre+Barcode+39
|
| You can just display a number using that font. Then hooked up a
| for-loop that's progressed by pressing the space bar: paste in
| IDs, scan first, space, scan next, repeat.
| oulipo2 wrote:
| Unrelated, but I bought a walking pad, and because I often use it
| at home while working on the laptop, and I wanted a nice UI to
| track the speed/time etc, I just asked claude to do one:
|
| https://pastebin.com/5HRLh1G6
|
| it does something like this
|
| https://imgur.com/a/888BtpG
|
| and connects through BLE
| fallinditch wrote:
| Another useful pattern for certain types of app is to include a
| function that saves the HTML file to your local drive/memory as a
| new file - for example, if the app features user inputs like
| writing or drawing.
| fallinditch wrote:
| It's a little problematic to share an HTML file that you made
| or saved on your phone with other phone users directly, for
| example by sending via a messaging app: Android it's ok, but
| iOS won't open or save an HTML file sent in this way.
| Apparently there is a workaround to long press on the file to
| save it to your Files folder first.
|
| This issue is relevant if your app's functionality includes the
| user changing the contents of the file and re-saving as a new
| file.
| simonw wrote:
| I just shipped a new one of these a few minutes ago (from my
| phone).
|
| I found out about a new Python HTML parsing library -
| https://github.com/EmilStenstrom/justhtml - and wanted to try it
| out but I'm out without my laptop. So I had Claude Code for web
| build me a playground interface for trying it out:
| https://tools.simonwillison.net/justhtml
|
| It loads the Python library using Pyodide and lets you try it out
| with a simple HTML UI.
|
| The prompts I used are in this PR:
| https://github.com/simonw/tools/pull/156
| dotancohen wrote:
| Serious question. How do you "try out" a library if somebody
| else (or something else) is writing the code?
|
| Thank you.
| simonw wrote:
| The thing that matters for me when considering a new library
| is what it can _do_. Once I know that I can decide if it 's
| worth learning how to use it at a code level.
|
| In the case of JustHTML I've now been able to try it against
| a few different HTML documents, seen it do good pretty-
| printing, played with its CSS selector implementation and got
| a feel for its event-based streaming parser. I'm very
| impressed! I think I'll be using it in the future next time I
| need an HTML parser.
| dotancohen wrote:
| I see, thank you.
|
| Until vibe coding came along, the ergonomics of a library
| were no less important than its functionality. But I
| understand how LLM assisted coding changes that
| perspective.
|
| I'll go tend to my empty lawn now.
| mirekrusin wrote:
| You can use react with jsx without any build step in single
| html file as well.
| simonw wrote:
| Yeah I used it for this one via Babel from a CDN but it felt
| pretty crufty: https://tools.simonwillison.net/box-shadow
| pseudosavant wrote:
| I love this pattern. I've been using it myself here:
| https://pseudosavant.github.io/ps-web-tools/
|
| Create PDFs from images, a Wordle hint/solver, or a classic DVD
| screensaver. Lots of stuff.
| chrisweekly wrote:
| > _" 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."_
|
| As if your steady stream of learning-in-public experiments and
| insights weren't generous enough. Seriously, massive kudos for
| sharing all the details.
| gaigalas wrote:
| Why not introduce a single shared CSS for style consistency? Not
| full CSS separation, each tool could still have its local CSS.
|
| Things like styling buttons, responsiveness, and so on are better
| solved once.
|
| A good rule of thumb is: if the shared CSS fails to load, page
| still fully works but it might be uglier (weird fonts, etc).
| That's a reasonable rule for proper isolation (tools remain
| simple to understand, code remains reusable, etc).
|
| I love the idea of self-contained tools, but you're already using
| CDNs. Having a shared CSS wouldn't hurt and actually make the
| tools better.
|
| I would go as far as having a shared JS too (same idea, works if
| it doesn't load).
|
| That's essentially what I did in
| https://alganet.github.io/spiral/ (also vibe coded).
|
| Each spiral is mostly independent. You can go ahead and delete
| the shared CSS from the <head>, they still work and don't break
| funcionality. However, by having the shared CSS I made them
| consistent, made them friendly to phone users and so on.
| simonw wrote:
| Yeah, I've been thinking some kind of reusable styles or style
| guide might be a good idea at this point.
|
| It's been fun collecting a bunch of inconsistent tool designs
| just to see how the different models behave, plus occasionally
| I go for something with a topical theme like
| https://tools.simonwillison.net/terminal-to-html or
| https://tools.simonwillison.net/new-yorker-style - but a little
| more consistency could be nice.
| gaigalas wrote:
| Definitely!
|
| Not only for the user, but it makes sense for the process of
| making the tools as well.
|
| If I left the agent for itself, it often come up with
| outrageous styles and I need to prompt it for something more
| sober.
|
| ---
|
| You can do a lot with just CSS. I restored this 2009 project
| of mine just now:
|
| https://alganet.github.io/ghiaweb/
|
| It still works (minor misalignments though), all HTML is pure
| (no class=, no css=, no <div>). The global CSS does
| everything: the forms, the drop-down menus, etc.
|
| Nowadays, we can do even better, no build step or anything
| like that.
| eliben wrote:
| Great work, Simon -- thanks for sharing!
|
| One tool I'd really like to see in this format is a simple "turn
| the background of this PNG to transparent". Models still refuse
| to follow the instruction to create transparent backgrounds for
| logos they create, and I often have to look for other tools doing
| this as post-processing.
|
| It's possible that this is too complicated for the "few hundred
| lines of js" code envelope, though.
| simonw wrote:
| Running this now... Build transparent-png.html
| - a tool that lets you open any image and then click on colors
| within that image to make them transparent - showing a preview
| of the resulting PNG against a checkerboard pattern and
| optional against other selected background colors below, plus a
| download PNG option It should also accept pasted
| images
|
| Here's what I got (from Opus 4.5 in Claude Code for web via the
| Claude iPhone app):
| https://tools.simonwillison.net/transparent-png
| indigodaddy wrote:
| Dude, you are amazing. Maybe you should make a suggestion
| box. Then while you are sleeping, the AI could evaluate the
| suggestions, and if they are good, it could prototype the
| tool, and then you could review the prototypes in your waking
| hours before clicking the tool to production. :)
|
| (I'm not actually kidding)
| eliben wrote:
| Nice, I'm proud I managed to nerd-snipe you :-) Thanks for
| taking the time.
|
| Seriously, though, I think this solves a nicely framed
| simpler problem. I was thinking about a more general tool,
| but that's genuinely hard (you'll need heavy CV algorithms or
| a special ML model to detect what is background what what
| isn't).
|
| To be honest, what you built here is probably sufficient
| anyway, because the models are better at obeying "create a
| white background" or "create a 0xffffff background" than
| "transparent", so this tool can post-process to what's
| needed.
|
| When asked for "transparent", I've had a model generate a
| fake checkerboard pattern of gray colors to imitate how
| viewers render transparent areas :-) For this kind of
| nonsense, the transparent-png tool wouldn't do!
| throwaway7783 wrote:
| In the same spirit I have started building single page nanodjango
| +htmx apps hosted on railway, starting with, you guessed it, a
| Todo list app. No login needed.
|
| https://web-production-1fc69.up.railway.app/
| bilater wrote:
| Have been doing the same except I do use React because its my
| hammer haha. But I agree if you're not used to it then pure
| html/cs/js make sense.
|
| https://www.hackyexperiments.com/micro
| soared wrote:
| Surprised AI studio isn't recommended here - you can go from
| prompt->preview->deploy in like a couple seconds.
|
| They have a library of sample apps you can edit but I wish they
| included the prompts and history to build each since I generally
| can't get large apps to work - after a while the I'll just
| produces more bugs as complexity grows. But I'm also a bad vibe
| coder and never read the code so entirely my fault :)
| simonw wrote:
| I don't trust it to give me share URLs that anyone can access
| without a Google account and that will work forever into the
| future.
|
| It may well do that, but it's not earned my trust yet!
| soared wrote:
| Ah I've only used that feature once actually I'll have to try
| it, I was talking about the deploy on gcp button (but
| pointlessly not free compared to your github pages recco!)
| indigodaddy wrote:
| I was displeased as well with the share url requiring login
| for AI studio.
| jackfranklyn wrote:
| The barcode font trick from valbaca is brilliant. I've done
| similar things with base64-encoded data URIs to make tools
| completely standalone - no network calls, no CDN dependencies.
|
| One pattern I've settled into: keeping tools under ~200 lines of
| JS total. Past that threshold I start losing the ability to hold
| the whole thing in my head, and the main benefit of these tools
| is that you can open them in a text editor and understand
| everything immediately.
|
| The CORS limitation that xnx mentions is real though. I've worked
| around it a few times by having tools accept paste-from-clipboard
| instead of fetching URLs directly. Less elegant but it keeps the
| tool self-contained and avoids the proxy problem simonw
| mentioned.
| ulrischa wrote:
| This was the first submission:
| https://news.ycombinator.com/item?id=46236333
| christophilus wrote:
| Nice. I do something similar, but with Bun + Preact, since I love
| that stack. I've found it very productive after the hurdle of the
| initial project setup (in which I wasted time crafting an RPC
| layer, etc, because I wanted to.)
| mettamage wrote:
| > 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.
|
| I haven't found too many issues with loading React and Babel from
| a CDN. I find React easier to read than straight HTML/JS. I find
| it more annoying to code in but seeing what state is needed in
| what components is a pleasant reading experience for me with
| single file tools.
| binsquare wrote:
| Yep - the author is very opinionated and it's good because it
| works really well for his workflows/usecase.
|
| I'm with you though, personally react is a acceleration
| mechanism for me because I often find existing well built
| components already. I don't built the same thing as the author
| though.
| cooljoseph wrote:
| I've done something similar for a couple tools.
|
| I tend to make them as Python servers which serve plain
| html/js/css with web components. I know this is a bit more
| complicated than just having a single html file with inline js
| and css, but the tools I made were a bit too complicated for the
| LLMs to get just right, and separating out the logic into
| separate js files as web components made it easy for me to fix
| the logic myself. I also deliberately prompted the LLMs to avoid
| React because adding I didn't want to need a build step.
|
| The only one I actually still use is the TODO app I made:
| https://github.com/cooljoseph1/todo-app It stores everything in a
| JSON file, and you can have multiple TODO lists at once by
| specifying that JSON file when you launch it.
| mettamage wrote:
| > HTML tools may not have access to server-side databases for
| storage but it turns out you can store a lot of state directly in
| the URL.
|
| I use indexedDB for it and will use sqlite if I start to get more
| serious data needs.
| calebm wrote:
| I've been calling these Single-File Web Apps. I've written a
| couple myself:
|
| [?] https://fuzzygraph.com [?] https://hypervault.github.io/
| calebm wrote:
| Slight correction: I'd only refer to it as a "Single-File Web
| App" if it has no external dependencies (where as the "HTML
| Tool" definition allows for external dependencies loaded from
| CDNs).
| blixt wrote:
| One thing I tend to do myself is use https://generator.jspm.io/
| to produce an import map once for all base dependencies I need
| (there's also a CLI), then I can easily copy/paste this template
| and get a self-contained single-file app that still supports JSX,
| React, and everything else. Some people may think it's overkill,
| but for me it's much more productive than
| document.getElementById("...") everywhere.
|
| I don't have a lot of public examples of this, but here's a
| larger project where I used this strategy for a relatively large
| app that has TypeScript annotations for easy VSCode use, Tailwind
| for design, and it even loads in huge libraries like the Monaco
| code editor etc, and it all just works quite well 100%
| statically:
|
| HTML file: https://github.com/blixt/go-
| gittyup/blob/main/static/index.h...
|
| Main entrypoint file: https://github.com/blixt/go-
| gittyup/blob/main/static/main.js
| rossant wrote:
| Awesome. This is the way.
| steren wrote:
| I've also been creating HTML tools over the years when I couldn't
| find client-side only websites for PDF to SVG, CSV to Sheet,
| Audio to Video, Video to MP4.
|
| I list them at https://client-side.app/
| meistertigran wrote:
| I was also heavy on these single-file HTML tools. My problem was
| not being able to sync the localstorage data between PC and
| smartphone. I made a paid service which did that automatically.
| It didn't take off unfortunately, and I am keeping it on
| maintenance mode because it has some amount of free users that
| rely on it.
|
| For anyone interested, to achieve synchronization I basically
| just use the https://github.com/google/diff-match-patch lib and
| save the patches in a db for each version with a version id. Then
| there's a generic JS file that I inject to uploaded HTML files
| that monkey patches the localstorage methods and optimistically
| updates the localstorage at the same time sending the diff to the
| server to save to the db.
___________________________________________________________________
(page generated 2025-12-13 23:00 UTC)