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 dollar.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-save
attribute - The user presses the
CMD/Ctrl+s
save 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
editmode
attribute to the<html>
element, set to eithertrue
orfalse
- 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)onresponse
— Executes after receiving a response, receivesres
objectonclone
— 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 — 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 usesnearest
under the hood, so it has a similar API:elem.val.some_selector
but it goes one step further. After finding the element that matches[some_selector]
, it returns the value of that attribute.text
— This usesnearest
under the hood, so it has a similar API:elem.text.some_selector
but it goes one step further. After finding the element that matches[some_selector]
, it returns theinnerText
of that element.exec
— This usesnearest
under the hood, so it has a similar API:elem.exec.some_selector
but 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 thetextContent
of 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
persist
attribute 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-input
attribute and they’ll automatically get adisabled
attribute for non-admins - Give any
script
or CSSlink
tag anedit-mode-resource
attribute and they’ll be inert for non-admins (though still viewable in “View Source”) - Attach an
edit-mode-contenteditable
attribute to any element and it will be editable only for admins - Attach an
edit-mode-onclick
attribute to any element with anonclick
and theonclick
will only trigger for admins - Attach
save-ignore
attribute 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 dollar.js, a concise library to use in
onclick
attributes- It combines array methods with DOM methods, so it’s easy to operate on large swaths of the DOM at once
- Call
$.section
to get all elements with a class or attributesection
and dump them in an array-like object that supports all DOM and array methods - Some examples of what you can do:
$.panel.classList.toggle('active')
finds all elements with the class (or attribute) “panel” and toggles.active
$.project.filter(el => el.dataset.status === 'draft').remove()
removes all.project
elements that have data-status=“draft”$.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 element$('.items').filter(el => el.dataset.active)
— Filter elements by condition$('.items').map(el => el.textContent)
— Map elements to array of values$('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
Mutation
is 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 = $.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
closest
andnearest
a 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
document
to 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
, dollar.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