autumn-plugin-vanilla
Declarative vanilla JS behaviors for Autumn apps. No npm, no bundler, no third-party JS.
- The plugin serves one small file,
vanilla.js, through the Autumn 0.10plugin_assetsseam. The file has a hashed URL, an immutable cache and SRI. - It works under the default CSP (
script-src 'self'). It uses noevaland no HTML strings. - It binds new content after each htmx swap. It cleans up before htmx removes content.
- Typed Rust builders write the attributes. The builders escape all values.
Requires autumn-web 0.10 and Rust 1.97.
Quickstart
Add the plugin:
use VanillaPlugin;
app
.plugin
.run
.await;
Put the script in the page <head>. The tag has defer and an SRI hash:
use vanilla_script;
html!
Use a builder:
use ;
html!
Or write the attributes yourself:
html!
Behaviors
A behavior acts on events from its element and from the descendants of that
element. One element can have many behaviors:
data-vanilla="confirm autosubmit".
| Behavior | Rust | What it does |
|---|---|---|
copy |
CopyText::selector(sel), CopyText::text(s) |
Copies text to the clipboard on click. |
toggle |
Toggle::target(sel).class(c) |
Shows or hides targets on click. Sets aria-expanded. |
dismiss |
Dismiss::new().after(d) |
Removes the element when the user clicks a close control, or after a time. |
confirm |
Confirm::new(msg) |
Asks before a form submits, a link opens, or an htmx element sends a request. |
autosubmit |
AutoSubmit::new().on_input().delay(d) |
Submits the form when a field changes. |
count |
Count::new().mode(m).output(sel) |
Shows the character count of a field. |
local-time |
LocalTime::new(&dt).format(f) |
Shows a <time> in the browser locale and time zone. |
All builders have attributes() and attr(name, value) for extra
attributes (id, class, aria-label, …). attr refuses event handler
names, style, type and data-vanilla… names. All builders except
Toggle and LocalTime have wrap(content), which puts content in a
<div>. Put toggle and copy on a <button>.
Attribute reference
| Attribute | Value | Default |
|---|---|---|
data-vanilla |
Behavior names, space-separated. | — |
data-vanilla-ignore |
On a container: nothing inside it binds. | — |
data-vanilla-copy |
CSS selector. Copies the value of a field, or the text of other elements. Never a password or hidden field. | — |
data-vanilla-copy-text |
Literal text. It has priority over data-vanilla-copy. |
— |
data-vanilla-copy-done |
Screen reader message on success. | Copied |
data-vanilla-copy-failed |
Screen reader message on failure. | Copy failed |
data-vanilla-toggle |
CSS selector. All matches toggle. | — |
data-vanilla-toggle-class |
Class to toggle. Without it, the hidden property toggles. |
— |
data-vanilla-dismiss-close |
On a descendant: the close control. | — |
data-vanilla-dismiss-after |
Milliseconds, a whole number. 0 sets no timer. |
no timer |
data-vanilla-dismiss-focus |
CSS selector. Focus goes there when the element closes with focus inside. | next sibling |
data-vanilla-confirm |
The question. | Are you sure? |
data-vanilla-autosubmit-on |
change, input, or both. |
change |
data-vanilla-autosubmit-delay |
Milliseconds to wait after the last event. | 0 |
data-vanilla-count |
CSS selector of the output. | the output inside |
data-vanilla-count-output |
Marks the output. The runtime writes only into a marked element. | — |
data-vanilla-count-mode |
used (12/280) or remaining (268). |
used |
data-vanilla-local-time |
datetime, date, time, relative. |
datetime |
State and events (all events bubble):
copysetsdata-vanilla-state="copied"or"failed"for 2 seconds and sendsvanilla:copiedorvanilla:copy-failed. A shared, hiddenrole="status"region announces the result. Style the state with CSS, for example[data-vanilla-state=copied]::after { content: " ✓"; }.dismisssends a cancelablevanilla:dismissevent before it removes the element. The timer pauses while the pointer or the focus is inside.autosubmitcallsform.requestSubmit(). An invalid form does not submit. Inchangemode, text fields do not submit (Enter already does). Only a form around the element, or the element itself, submits.local-timesets the text one time, when it binds. A date without a time (2026-10-05) shows that calendar day in every time zone.
Accessibility
togglesetsaria-expanded.Toggle::buttonaddsaria-controlsfor a plain#idtarget. Puttoggleon a<button>.copyannounces the result through arole="status"region.dismissmoves focus when it removes a focused element. Do not useafterfor errors or other important text. Addrole="status"with.attr("role", "status"), so screen readers announce a new flash.Count::output_slot()hasaria-live="off", so a screen reader does not read each key press. Point the field at it witharia-describedby.- A submit that loads a new page is a change of context (WCAG 3.2.2). Use
autosubmitwith an htmx partial swap, or tell the user before the field.
Security
The runtime binds any data-vanilla element in the page. User HTML with
these attributes can use the behaviors. If your app shows user HTML:
- Remove
data-vanilla*attributes in the sanitizer. Ammonia removes them by default. DOMPurify needsALLOW_DATA_ATTR: false. - Or put that HTML inside
[data-vanilla-ignore].
Limits that the runtime applies:
copynever reads a password or hidden field.countwrites only into an element withdata-vanilla-count-output.autosubmitsubmits only a form around its element.confirmasks in the capture phase, before htmx.- A
<a id="Vanilla">or<img name="querySelector">in the page cannot replace the runtime or its queries.
Selectors and messages that you give to the builders are trusted input. Do not make a selector from user data.
Add your own behavior
Put the code in a same-origin file. Load it after vanilla_script(), with
defer:
// static/js/app.js
;
html!
window.Vanilla API:
| Function | Description |
|---|---|
register(name, init) |
Adds a behavior. name is lowercase ASCII, digits and -. init(el) can return a cleanup function. A late call binds the elements already in the page. |
scan(root) |
Binds all data-vanilla elements in root (default: document). Each element and behavior binds one time only. |
teardown(root) |
Runs the cleanup of root and of each element inside it. A later scan binds them again. |
names() |
The registered behavior names, sorted. |
htmx integration:
- On
htmx:load, the runtime binds the new content. It also binds each bound ancestor again (cleanup, then init), because its content changed. - On
htmx:beforeCleanupElement, it runs the cleanup of that element.
If a behavior throws, the runtime logs the error and continues with the other elements. It does not try that behavior on that element again.
Demo
# Open http://127.0.0.1:3000
How it works
assets/vanilla.jsis the runtime and the 7 behaviors.VANILLA_ASSETSis aPluginAssetsbundle with that one file.VanillaPlugininstalls it withAppBuilder::plugin_assetsand declares aPluginContractfor autumn-web0.9.- Autumn serves the file under
/static/_plugins/vanilla/: at a hashed URL (immutable) and at a plain URL (must-revalidate), withETag/304. vanilla_script()writes the hashed URL and the computed SRI hash.asset_url("_plugins/vanilla/vanilla.js")also gives the hashed URL.
See docs/adr/0001-plugin-assets-seam.md and docs/plan.md.
Limits
- No modal, tabs, autocomplete or nav. Autumn ships these in
autumn-widgets.js. confirmuseswindow.confirm.- An idiomorph swap keeps elements. The runtime binds the swap target and its ancestors again, but not kept descendants.
- The Clipboard API needs a secure context (HTTPS or localhost). Elsewhere,
copyusesdocument.execCommand('copy').
Development
&& &&
CI runs all of these, plus MSRV, coverage (≥ 85% lines) and a publish dry
run. Branch protection should require the CI pass check.
License
Apache-2.0