A file that saves itself.
A checklist on your own disk that keeps every word you type and every box you check. No account, no build step, nothing to configure. Four steps, and once HTML Clay is installed, wiring the file takes about two minutes.
The four steps need a macOS, Windows, or Linux computer and an internet connection. The demo right below is the part that works anywhere, phones included.
My list
- Buy oat milk
- Book the dentist
- Water the fig tree
Rewrite a task, check a box, then just stop. Autosave is on, the same way it will be in your file. Reload the page and both the words and the checkmarks are still here.
This copy saves into your browser's storage through the real save pipeline; a save normally writes to disk. The steps below give you the real thing: a file on disk that does it.
See the HTML a save writesclay.getHTML()
This demo sheet's slice of the exact string clay.getHTML() returns: the payload a save sends, after cleanup. Your edits are just markup now.
Check a box above and watch this block: a checked attribute appears on that input as soon as the save lands. That attribute is the whole trick, and it is what will show up in your file at the end of Step 4.
2 · Download my-list.html, rename it .htmlclay → double-click opens it, served
3 · Add one script tag → the page can save itself
4 · autosave + editable + persist → words and checkmarks land in the file
Step 1 · Get HTML Clay
A browser can't write to a file on your disk.
Something outside the browser sandbox has to hold the pen. HTML Clay is that something: a small desktop app whose whole job is to serve your file locally and write saves back into it. No account, and it only ever writes inside your home folder: the file you opened, or a folder you explicitly trust.
HTML Clay is a desktop app, so the four steps can't finish on this phone. The demo above is the part that works here. Come back on a Mac, Windows, or Linux machine when you have five minutes.
Install it and open it once before Step 2. On macOS drag it into Applications; on Windows unzip it and run htmlclay.exe; on Linux unpack the tarball and run bash install.sh. That first launch is what teaches your computer to open .htmlclay files, so the double-click in Step 2 has something to hand the file to.
Free. macOS universal dmg, about 8.6 MB · Windows 64-bit zip, about 3.7 MB · Linux tar.gz, about 4.3 MB, x86_64 and ARM64. All downloads.
The macOS build is signed and notarized and the Windows builds are signed, so neither needs a security-warning workaround. The Linux builds are not signed. On macOS the button goes through htmlclay.com's always-current download list because the dmg's file name carries its version number; on Linux it goes there because only you know whether you want x86_64 or ARM64.
Step 2 · Get the starter
Double-clicking an HTML file just views it.
my-list.html is ordinary HTML for now: a heading, three checkboxes, a few lines of CSS. No clayjs, no script tag, nothing that saves. Steps 3 and 4 are what change that, and doing them yourself is the whole point of this guide.
Then, in this order:
- Leave it in Downloads. Downloads sits inside your home folder, and inside your home folder is the only place HTML Clay will serve from, so the file already works right where it landed.
- Rename it to
my-list.htmlclay. Same file, different extension. That extension is the only thing that tells your computer to hand it to HTML Clay. - Double-click it. HTML Clay serves it at
http://127.0.0.1and opens it in your default browser, as an ordinary tab. Right now it is still just a page; the next two steps make it a document.
Watch the extension. On Windows, turn on View > Show > File name extensions before you rename, or Explorer quietly produces my-list.htmlclay.html and the double-click opens a plain browser tab with no app involved. On a Mac, Finder asks whether to keep .html: choose Use .htmlclay.
Here is the whole file, if you would rather see it first or type it out yourself.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My list</title>
<style>
body {
font: 17px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
color: #2B2723; background: #F1EFEA;
max-width: 30em; margin: 0 auto; padding: 7vh 22px 12vh;
}
h1 { font-size: 30px; line-height: 1.2; margin: 0 0 4px; }
p.sub { color: #57514A; margin: 0 0 26px; }
ul { list-style: none; margin: 0; padding: 0; }
li { display: flex; align-items: center; gap: 12px; padding: 13px 2px; border-bottom: 1px solid #DDD8CE; }
input[type=checkbox] { width: 19px; height: 19px; flex: none; accent-color: #3E6B52; cursor: pointer; }
input[type=checkbox]:checked + span { color: #57514A; text-decoration: line-through; }
:focus-visible { outline: 2px solid #3E6B52; outline-offset: 3px; }
</style>
</head>
<body>
<h1>My list</h1>
<p class="sub">What I need to get done.</p>
<ul>
<li><input type="checkbox" aria-labelledby="t1"> <span id="t1">Buy oat milk</span></li>
<li><input type="checkbox" aria-labelledby="t2"> <span id="t2">Book the dentist</span></li>
<li><input type="checkbox" aria-labelledby="t3"> <span id="t3">Water the fig tree</span></li>
</ul>
</body>
</html>
The Copy button and the Download button hand over the same bytes. Any HTML file you already own works exactly the same way from here.
On a machine without the app, nothing is lost: it is still an ordinary HTML file. Rename it back to .html and any browser opens it. Nothing about the file depends on the app.
Step 3 · Add the script tag
The page doesn't know how to save itself yet.
<script src="https://clayjs.com/v1/clay.js"></script>
Open my-list.htmlclay in your text editor and add that line just before </body>. Keep the editor open and go straight to Step 4. It adds three attributes to the same file, and then you reload the tab once and see everything arrive at the same time.
Here is what that one line buys. When HTML Clay serves the file, it stamps a save token onto <html>. clayjs reads that token, switches into edit mode on its own, and knows to POST the whole document back to the app. No config, no save code to write.
This tag loads clayjs from clayjs.com, so the tutorial path needs an internet connection. Your file and your saves never leave your machine; the library is the only thing that crosses the network. If you want nothing to cross it, the standalone build is the whole library in one file you keep beside yours.
Step 4 · Turn on editing and saving
Nothing on the page is editable, and nothing is being saved.
Three attributes, three different jobs. Add all three to the file you still have open in your editor, then reload once and prove it.
-
autosave
Saves the whole document a moment after your edits settle: 1.5 seconds after the last change, throttled so a fast typist gets one save rather than fifty. ⌘S or Ctrl+S always works too.
<html lang="en" autosave>Once, on
<html>. It is the second line of the file. -
editable="single-line"
Makes the text inside an element typable, in place. The
single-linetoken keeps Enter from splitting the element into block markup, which is exactly what a one-line checklist row wants.<h1 editable="single-line">My list</h1> <span id="t1" editable="single-line">Buy oat milk</span>On the heading and on all three task spans. Never on the
<li>and never on the checkbox: a checkbox inside an editable region becomes something you can select, drag, and delete by accident. -
persist
A checkbox's checked state lives in the browser's memory, not in the HTML, so a plain save would lose it.
persistcopies that live state into the tag as a realcheckedattribute, and removes it again when you uncheck.<input type="checkbox" persist aria-labelledby="t1">On all three checkboxes. The
aria-labelledbyis already in the starter; it points at the task text so the checkbox keeps an accurate name even after you rewrite the task.
Which makes each task row read like this:
<li><input type="checkbox" persist aria-labelledby="t1"> <span id="t1" editable="single-line">Buy oat milk</span></li>
Now prove it
Eight moves, one reload, and you will have watched a browser write to your disk.
- Save the file in your text editor.
- Reload the browser tab. Once. This is the only reload the guide asks for.
- Click the heading, or any task, and type something of your own.
- Check a different task, so the two changes can't be confused for each other.
- Wait about two seconds without touching anything. That is autosave settling.
- Go back to your text editor and open
my-list.htmlclayagain, or reload the file if it is still open. - Find both things: your new words in the heading or the span, and a
checkedattribute on the box you ticked. - Reload the browser tab one more time. The new words and the checkmark are both still there, because they were never anywhere but the file.
The line you are hunting for in move 7. A browser writes bare attributes back with
empty values and appends checked at the end, so it reads like this rather
than the way you typed it:
<li><input type="checkbox" persist="" aria-labelledby="t2" checked=""> <span id="t2" editable="single-line">Book the dentist</span></li>
Uncheck that task, wait, and look again: the attribute is gone. Your list's state is not in a database and not in browser storage. It is in the file, spelled out in HTML you can read.
If your editor still shows the old text, reload the file: an editor that opened it back in Step 3 may be showing you the buffer it read then. VS Code refreshes on its own; some editors want a nudge. Also new in the file: <html> has gained an htmlclayid attribute. That is the app's durable name for the file, and it is the only thing the app adds.
While it works, clayjs keeps a savestatus attribute on <html> up to date (saving, saved, error, offline) and draws no interface of its own. Style it with a few lines of CSS, listen to the clay:save-* events (that is what this page's status chip does), or load a ready-made chip with clay.js?plugins=indicator. Details in the reference.
Something isn't landing?the finished file
Every attribute in place. Paste it over the whole file, save, reload the tab, and run the proof again. It is here to get you unstuck, not to replace Step 4: putting the three attributes in by hand is what makes the next file, the one that is actually yours, easy.
<!DOCTYPE html>
<html lang="en" autosave>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My list</title>
<style>
body {
font: 17px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
color: #2B2723; background: #F1EFEA;
max-width: 30em; margin: 0 auto; padding: 7vh 22px 12vh;
}
h1 { font-size: 30px; line-height: 1.2; margin: 0 0 4px; }
p.sub { color: #57514A; margin: 0 0 26px; }
ul { list-style: none; margin: 0; padding: 0; }
li { display: flex; align-items: center; gap: 12px; padding: 13px 2px; border-bottom: 1px solid #DDD8CE; }
input[type=checkbox] { width: 19px; height: 19px; flex: none; accent-color: #3E6B52; cursor: pointer; }
input[type=checkbox]:checked + span { color: #57514A; text-decoration: line-through; }
:focus-visible { outline: 2px solid #3E6B52; outline-offset: 3px; }
</style>
</head>
<body>
<h1 editable="single-line">My list</h1>
<p class="sub">What I need to get done.</p>
<ul>
<li><input type="checkbox" persist aria-labelledby="t1"> <span id="t1" editable="single-line">Buy oat milk</span></li>
<li><input type="checkbox" persist aria-labelledby="t2"> <span id="t2" editable="single-line">Book the dentist</span></li>
<li><input type="checkbox" persist aria-labelledby="t3"> <span id="t3" editable="single-line">Water the fig tree</span></li>
</ul>
<script src="https://clayjs.com/v1/clay.js"></script>
</body>
</html>
Three parts, one document
The file owns everything: the interface and the state are one HTML document. clayjs, inside the page, turns your edits into a clean serialized copy of that document. HTML Clay, outside the browser, is the only part that touches disk: it serves the file up and writes the saves back.
The save is one POST of the whole document, as plain text, to /_/save/<token> on the local app. What lands on disk is HTML you can read. The same one-route protocol runs hyperclay.com and about 20 lines of your own server.
clayjs is MIT No Attribution: use it, change it, ship it, no notice required. Its terms are its LICENSE. HTML Clay is source-available, not open source, under the First Million Stays Yours License: no fee on your first $1 million of covered revenue in a year, 3 percent of the amount above that, and each release becomes MIT 18 months after it ships.
You have a file. Now make it an app.
The reference: API, attributes, events → /docs
Host it anywhere: the one-route endpoint spec → /docs#endpoint
Everything the app does: versions, live sync, the wire → htmlclay.com/features
Is it safe? The exact boundary → htmlclay.com/#safe
Honest questions, honest answers
Isn't this just contenteditable plus a POST?mostly, yes
Mostly, yes. contenteditable has been in browsers for twenty years, and the save is one POST of the whole document. The news is what is missing: no format of its own, no export step, no cloud in the loop. Double-click a file, edit it like a document, and the exact HTML you were looking at is written back to the same file, clean enough to read. clayjs earns its keep on the unglamorous parts, a clean serialize with the runtime junk stripped, form state written into the markup so a checked box survives, autosave that settles instead of spamming, a warning before you close unsaved work, but the core is as simple as you suspect. That simplicity is the point.
A desktop app is a bigger ask than a script tag.it is
It is. But a web page cannot write to your disk; the sandbox is the one rule this idea cannot cross, so a native process has to do the writing. HTML Clay is deliberately small about it: 4 to 9 MB, no account, and it only ever writes inside your home folder, into the file you opened or a folder you explicitly trust. It also brings more than the pen: every file it opens gets automatic version history, a local program can work on the file through the wire, and an edit made in your text editor or by an agent can merge into the open page with no reload at all. That last one is the sync plugin rather than the default, so it wants clay.js?plugins=sync; the four steps above stay on a plain reload. The full list is at htmlclay.com/features. If you'd rather install nothing, hyperclay.com hosts the same files, saved to their disk instead of yours.
What actually gets sent when it saves?the whole file
The entire document, serialized as HTML and POSTed as plain text to the local app. There is no hidden project format and no sidecar files: what lands on disk is the same kind of HTML you would write by hand. clay.getHTML() returns the exact string, and the fold at the top of this page shows it live.
View source shows attributes I didn't write.two, one stays
When HTML Clay serves your file it puts two attributes on <html>: htmlclaytoken, a per-serve save credential, and htmlclayid, a durable identity for the file. clayjs adds savestatus while it works. Before anything is written to disk, htmlclaytoken and savestatus are stripped. htmlclayid is kept: it is how the app still recognizes the file after you rename or move it. So the browser shows three attributes you didn't write, and the file on disk gains exactly one.
Already have a static file?
That checklist is only an example. The same four steps work on any HTML you own: a page you already have, or the personal software you have been meaning to build. Paste the prompt below at a coding agent (Claude, Cursor, Codex…), with your file if you have one, and it does steps 3 and 4 for you.
Type it and the prompt below asks the agent to build it first. Leave it empty if you're bringing a file you already have.
Make this static HTML file malleable with clayjs. Read https://clayjs.com/llms.txt before you start: it is the full clayjs reference, including every plugin and advanced module. Add <script src="https://clayjs.com/v1/clay.js"></script> as the last element inside <body>. Add an autosave attribute to the <html> element. Add an editable attribute to each element a person should be able to edit as rich text (headings, paragraphs, note containers), and add a persist attribute to any form control whose value should survive saving. Do not write any save logic or add a save button: clayjs saves the whole document automatically, and the host (the HTML Clay desktop app for .htmlclay files, or hyperclay.com) writes it to disk. If part of the page is UI that should never be written to disk, mark it clay="no-save no-watch". Change nothing else.