Build HTML Apps on Hyperclay
Paste this page into your brain or an LLM to generate an up-to-date, robust HTML app you can host on Hyperclay.
How Hyperclay Works
We want to create a special kind of application that uses HTML as both the front end and the database. Whenever the page changes—or the user explicitly saves the page—we grab all the HTML, make a few modifications, and then POST it to the backend’s “save” endpoint.
The modification usually made before the page is saved is removing any admin controls. That way, when the page is reloaded by a non-logged-in user (an anonymous viewer), they’ll only see a static, read-only page.
When an admin loads that same page, they get the view-only page first, then any additional controls or editing features load in.
That’s the core idea of Hyperclay: a single HTML file that can toggle between view-only mode and edit mode, where changes made in the UI are persisted to the backend.
jQuery is Your Starting Point
The first thing you should reach for is jQuery. Just make a page that can edit the DOM and Hyperclay will handle the rest. jQuery is a great fit for Hyperclay - it’s battle-tested, familiar, and perfect for DOM manipulation.
Simply include jQuery in your HTML and start building. You don’t need anything else to get started. Create elements, modify them, delete them - Hyperclay automatically detects DOM changes and persists them.
More advanced solutions like using the utilities in the Hyperclay Starter Kit are described below, but start simple with jQuery. Build your app with basic DOM manipulation first, then add the advanced features as needed.
We also provide all.js as a modern alternative that combines array methods with DOM methods, but jQuery remains an excellent choice.
The Save Lifecycle
The “save lifecycle” is the heart of Hyperclay. Everything in the hyperclay-starter-kit.js (or the underlying file it references hyperclay.js) revolves around that concept.
Here it is: the page changes, before saving we strip out admin-only elements, then we save the page. When you load the page, we re-inject the admin-only elements if you’re an admin.
Hyperclay is perfect for building front-end–only apps: you don’t need to worry about user accounts or a separate backend—just HTML, vanilla JS, and vanilla CSS in a single file that others can download, view, and even edit if they become the owner.
The Hyperclay Starter Kit
There’s a script called the Hyperclay Starter Kit (located at /js/hyperclay-starter-kit.js) that sets up the basics for your single-file HTML app:
- Automatic save of the entire DOM when:
- The DOM changes (this includes changes made in the browser’s DevTools)
- The user clicks a button with a
trigger-saveattribute - The user presses the
CMD/Ctrl+ssave keyboard shortcut
- Visibility rules for edit mode and view mode
- Hyperclay ships with a utility that automatically shows/hides elements with
option:attributes based on whether any ancestor has a matching regular attribute (e.g.,<div option:editmode="true">is shown inside<div editmode="true">) - On page load, Hyperclay adds an
editmodeattribute to the<html>element, set to eithertrueorfalse - The
option:system works with ANY attribute/value pair for dynamic visibility control (e.g.,<style option:theme="dark">shows when inside<div theme="dark">)
- Hyperclay ships with a utility that automatically shows/hides elements with
- Support for custom event attributes that make working with the DOM easier
onrender— Evals its code when the element is rendered, usually on page load. Good for setting up the page before users interact with it.onbeforesave— Evals its code before the page is saved byhyperclay.js. Good for removing admin UI before saving the page, as the version of the page you want to save should always be in view-mode. e.g.<div onbeforesave="this.remove()">onclickaway— Evals its code when the user clicks somewhere that is not this current element.onpagemutation— Evals its code when any DOM mutation occurs anywhere on the pageonbeforesubmit— Executes before form submission (can return a Promise), use withajax-formonresponse— Executes after receiving a response, receivesresobject, use withajax-formonclone— Executes when element is cloned (useful for dynamic lists)
- Support for custom DOM properties, accessible on every element
sortable— Uses sortable.js to create a sortable container. All the elements inside of it can be dragged and reordered. The attribute value is the group name, so it can support dragging between two lists in the same group:sortable="tasks"allows dragging between multiple lists with the same group name.sortable-handle— Define a drag handle within sortable items (e.g.,<div sortable-handle>⋮⋮</div>)nearest— This is a strange but incredibly useful attribute. It’s used like this:elem.nearest.some_selector. It searches all nearby elements for an element with a custom attribute that matches[some_selector]or has the class.some_selector. It’s useful because you don’t have to think about if that element is a direct ancestor or sibling or child — you just ask it to get you the nearest one.- Here’s how I use this on panphora.com:
this.nearest('.project').before(this.nearest('.project').cloneNode(true)), this finds the nearest.project, clones it (including its children), and inserts it before the original element—useful for duplicating a project block.
- Here’s how I use this on panphora.com:
val— This usesnearestunder the hood, so it has a similar API:elem.val.some_selectorbut it goes one step further. After finding the element that matches[some_selector], it returns the value of that attribute.text— This usesnearestunder the hood, so it has a similar API:elem.text.some_selectorbut it goes one step further. After finding the element that matches[some_selector], it returns theinnerTextof that element.exec— This usesnearestunder the hood, so it has a similar API:elem.exec.some_selectorbut it goes one step further. After finding the element that matches[some_selector], it evals the code in the value of that attribute.
- Support for custom DOM methods, accessible on every element
cycle(order, attr)— This is a strange and very useful attribute. It allows you to replace an element with the next or previous element of its same type, the type being specified byattr. In order to find the next unique element of the same type, it compares thetextContentof each element.cycleAttr(order, attr)— This is similar tocycle, but instead of replacing the entire element, it just cycles the value of the attribute.
- Enable persistent form input values by attaching a
persistattribute to any input or textarea element- For example, if you check a checkbox and you’re an admin, those changes persist to the DOM and thus the backend
- Additional form and UI attributes
prevent-enter— Prevents form submission when Enter key is pressed (useful for multi-line inputs)autosize— Auto-resizes textarea elements based on their content
- Admin-only attributes
- Give any input or textarea the
edit-mode-inputattribute and they’ll automatically get adisabledattribute for non-admins - Give any
scriptor CSSlinktag anedit-mode-resourceattribute and they’ll be inert for non-admins (though still viewable in “View Source”) - Attach an
edit-mode-contenteditableattribute to any element and it will be editable only for admins - Attach an
edit-mode-onclickattribute to any element with anonclickand theonclickwill only trigger for admins - Attach
save-ignoreattribute to any element to have it be removed from the DOM before saved and have DOM changes to it be invisible to hyperclay
- Give any input or textarea the
- One of the objects exported from the starter kit is
hyperclay, which comes with some useful methods:beforeSave— Called before the page is saved, receives the document element (which you can modify) as its one argument, useful for stripping admin controls to maintain a “clean” initial version of the pageisEditMode()— Returns boolean indicating if currently in edit modeisOwner()— Returns boolean indicating if current user owns the sitetoggleEditMode()— Toggle between view and edit modesuploadFile(eventOrFile): Uploads a file from either a file input event or File object, showing progress toasts and copying the URL on completioncreateFile(eventOrData | {fileName, fileBody})— Creates and uploads a file from either a form event, data object, or direct parameters, with progress feedback. Returns{url, name}on success.uploadFileBasic(eventOrFile, {onProgress?, onComplete?, onError?})— Bare-bones file upload with customizable progress/completion/error callbacks instead of built-in toast notificationssavePage(callback?)— Saves the current page HTML to the server if changes detected, takes optional callback that runs after successful savesendMessage(eventOrObj, successMessage, successCallback?)— Sends a message from an anonymous viewer to the admin of the page, only if they’re likely to be human and not a bot. If passing in a submitevent, all form fields will be sent. Otherwise, object will be converted to JSON and sent.
- Concise DOM manipulations with All.js, a concise library to use in
onclickattributes- It combines array methods with DOM methods, so it’s easy to operate on large swaths of the DOM at once
- Call
All.sectionto get all elements with a class or attributesectionand dump them in an array-like object that supports all DOM and array methods - Some examples of what you can do:
All.panel.classList.toggle('active')finds all elements with the class (or attribute) “panel” and toggles.activeAll.project.filter(el => el.dataset.status === 'draft').remove()removes all.projectelements that have data-status=“draft”All.project.filter(el => el !== this && el.text.project_name === this.text.project_name).replaceWith(this.cloneNode(true))replaces all[project]elements on the page with the current elementAll('.items').filter(el => el.dataset.active)— Filter elements by conditionAll('.items').map(el => el.textContent)— Map elements to array of valuesAll('ul').onclick('li', function() {...})— Event delegation for dynamic content- For more advanced examples, look at the source code for panphora.com
- These UI helper methods are also exported by the starter kit
ask(promptText, yesCallback?, defaultValue?, extraContent?)— Shows a modal dialog with text input, returns a Promise that resolves to input value, rejects if cancelled, callback runs on confirmconsent(promptText, yesCallback?, extraContent?)— Shows a yes/no confirmation modal dialog, returns a Promise that resolves on confirm, rejects if cancelled, callback runs on confirmtoast(message, messageType?)— Shows a temporary notification message with optional type (‘success’ or ‘error’), auto-dismisses after 6.6s or on clickinfo(message)— Shows an information dialog
- Other useful DOM helpers
Mutationis exported, which can track changes to the DOM. It’s used to save the page whenever the DOM changes. To have it ignore an element (and its children), attach the attributemutations-ignore. It has a wider API, but here’s an example of how to use it:Mutation.onAnyChange({debounce: 200, omitChangeDetails: true}, () => {})
- Additional global utilities available
nearest(element, '.selector')— Find nearest matching element (standalone version)slugify('Hello World!')— Convert text to URL-friendly slug (“hello-world”)h('div.container>h1{Title}+p{Content}')— Emmet-style HTML generationgetTimeFromNow(date)— Format dates as relative time (“2 hours ago”)getDataFromForm(formElement)— Serialize form data to objectcookie.set('key', 'value')/cookie.get('key')— Cookie managementquery.get('param')/query.set('param', 'value')— Query parameter management
Multi-tenant capabilities
Enable signups through your dashboard in the app settings menu to transform your app into a multi-tenant platform, allowing multiple users to have their own instances.
Tailwind support
It’s very easy to add support for Tailwind by including the styles /css/tailwind-base.css and the script /js/vendor/tailwind-play.js. It’s pretty much the same as the one from the Tailwind play CDN , except we make sure it uses the same style tag every time (instead of creating a new one) and we strip out some initial styles and put them in tailwind-base.css so they don’t pollute the DOM.
Apps with lists of items
When creating apps that have lists of items, you’ll want to be able to create new items with default values. To stick with the best practice of using the DOM as the source of truth, it’s strongly recommended to create an item at the start of the list set to display: none with all of the default values you want. Creating an item is then as simple as: onclick="let card = all.card.at(0); card.classList.remove('hidden'); this.nearest.list.append(card.cloneNode(true)); toast('Card added');"
Apps with complex data
If you need to store data in an intermediary format like JSON (discouraged — try to keep things in the DOM), you can use a <script type="application/json"> tag as a database you can read and write to.
Tip: if you need to store HTML that includes script tags, escape the script tags so they don’t prematurely end the script tag you’re using as a database: str.replaceAll("</script>", "[/SCRIPT]") and then decode it when using it: str.replaceAll("[/SCRIPT]", "</script>")
Why doesn’t Hyperclay just implement a simple key/value database? Because we’d like to maintain the ability for people to download a single, portable HTML file that works as a portable app on its own, with as few dependencies as possible.
File upload and form submissions
Use hyperclay.uploadFile to for uploading files (only works if you’re the page owner). Accepts multiple files or base64 data. Returns {url, name} on success.
Use hyperclay.sendMessage to allow visitors to send the app admin a message (works for anonymous visitors). This will submit basic behavior data about the user to the server, which the server will use to confirm they’re human.
Tips
- Think of the DOM as a tree where nodes that are higher up in the tree are natural components. That means using
closestandnearesta lot and setting state on parent elements in order to control the style and behavior of their children. - When dynamically adding CSS, if you want to avoid flashes of unstyled content, add the new styles before removing the old ones.
- Use event delegation on
documentto handle all click/input/submit events, so when the DOM is mutated your event handlers keep working.
Security
Worried about allowing people to run their own code on their own sites? It’s the same security model as Wordpress/SquareSpace or any other website builder, which all allow you to include arbitrary HTML and JS. We trust the owner of each app to manage their own code and content and we report it to authorities and take it down if it’s illegal or harmful to others.
Wrap-Up
That’s pretty much it. Hyperclay’s mutation detector watches for page changes, triggers a save, and the code strips out the admin controls so the default view mode is clean. We rely on custom attributes (e.g. onrender, onclickaway, onbeforesave, trigger-save, ignore-save, edit-mode-contenteditable, edit-mode-onclick), built-in event attributes (onclick, oninput, etc.) and libraries (hyperclay-starter-kit.js, all.js) to build our app functionality in a single HTML file.
You can add attributes like onbeforesave="someCleanupFunction()" or edit-mode-onclick="doAdminThing()" to seamlessly handle admin vs. viewer behavior.
It’s a lightweight but powerful approach for building front-end-only, persistently malleable experiences that are portable, editable, shareable, and personal — perfect for apps generated by LLMs that take an afternoon of prototyping and iterating, when you don’t want to spin up a full, traditional backend just to deploy something cool.
- Write a few lines of JS + HTML
- Hyperclay handles persistence and access control
- You get a great app with 0 time spend fiddling with web services