Hyperscript
_hyperscript is an event-oriented scripting
language inspired by HyperTalk. It is small, easy to read inline in
markup, and pairs cleanly with htmx. Hypermedia Components does not
require it — the vanilla behaviors in @hypermedia-components/core are
the default — but every behavior has a natural _hyperscript equivalent
if you prefer to keep the logic next to the markup. And the interactive
components emit hc:* events you can react to inline — see
Reacting to component events.
Use this page as a translation reference: copy a snippet, paste it
into an existing _= attribute (or a <script type="text/hyperscript">
block), and skip the matching installXxx() call.
Asset loading
Section titled “Asset loading”Add _hyperscript after htmx and before any markup that uses _=
attributes.
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="/assets/hc/hc.css">
<script defer src="https://unpkg.com/htmx.org@2"></script> <script defer src="https://unpkg.com/hyperscript.org@0.9"></script>
<!-- Only the bits of HC you still want. See "Mixing approaches" below. --> <script type="module" src="/assets/hc/hc.behaviors.min.js"></script> </head> <body> ... </body></html>_hyperscript self-installs at DOMContentLoaded. There is no
configuration step.
Philosophy
Section titled “Philosophy”The vanilla installXxx behaviors and the _hyperscript snippets below
do the same thing — intercept events, talk to native DOM APIs, never
wrap fetch(). The choice is ergonomic:
- Vanilla wins when the same behavior shows up on many elements
(one
installConfirm()call, every[data-hc-confirm]is handled), when you want JSDoc / TypeScript types on the helper, or when the logic outgrows a one-liner. - _hyperscript wins when the logic is local to a single element,
when you want the markup to read like prose, or when you cannot
ship a bundle that includes
hc.behaviors.min.js.
Both compose with htmx the same way: dispatch an event, let htmx
listen for it via data-hx-trigger.
Mixing approaches
Section titled “Mixing approaches”You do not have to choose globally. Drop the installXxx you are
replacing — keep the others. For example, to replace just the confirm
behavior with inline _hyperscript while keeping the vanilla toast
region, import the individual installers:
import { installToast, installCloseDialog, installClosePopover, installRemoteDialog,} from '@hypermedia-components/core';
installToast();installCloseDialog();installClosePopover();installRemoteDialog();// installConfirm() intentionally skipped — _hyperscript handles confirm inline.The @hypermedia-components/core/behaviors auto-init entry installs
every default behavior at once. Skip that import when you are picking
and choosing, and call the individual installX() functions you want.
Equivalents
Section titled “Equivalents”Confirm action
Section titled “Confirm action”Vanilla — driven by installConfirm():
<button class="hc-button" data-variant="error" data-hc-confirm="Delete this item?" data-hx-delete="/items/123" data-hx-trigger="hc:confirmed" data-hx-target="closest tr"> Delete</button>_hyperscript — handle the confirmation inline; htmx still listens for
hc:confirmed:
<button class="hc-button" data-variant="error" data-hx-delete="/items/123" data-hx-trigger="hc:confirmed" data-hx-target="closest tr" _="on click if confirm('Delete this item?') send hc:confirmed to me end"> Delete</button>confirm(...) is the browser primitive — synchronous, blocking, and
not styleable. To use the shared <dialog class="hc-confirm-dialog">
markup instead, render it once in the page and target it by id:
<dialog id="confirm-dialog" class="hc-dialog hc-confirm-dialog" aria-labelledby="confirm-title" aria-describedby="confirm-message"> <header class="hc-dialog__header"> <h2 class="hc-dialog__title" id="confirm-title">Confirm</h2> </header> <div class="hc-dialog__body" id="confirm-message">Are you sure?</div> <footer class="hc-dialog__footer"> <button class="hc-button" type="button" _="on click call closest <dialog/> then call it.close('cancel')"> Cancel </button> <button class="hc-button" type="button" data-variant="primary" _="on click call closest <dialog/> then call it.close('confirm')"> Confirm </button> </footer></dialog>
<button class="hc-button" data-variant="error" data-hx-delete="/items/123" data-hx-trigger="hc:confirmed" data-hx-target="closest tr" _="on click set #confirm-message.textContent to 'Delete this item?' call #confirm-dialog.showModal() wait for close from #confirm-dialog if #confirm-dialog.returnValue is 'confirm' send hc:confirmed to me end"> Delete</button>The _hyperscript variant gives you the same a11y story
(<dialog showModal> handles focus trapping and Escape), but you
write the orchestration in the script attribute instead of relying on
a shared installed behavior. See
the confirm-action recipe
for the same example in context.
Toast trigger (client-side)
Section titled “Toast trigger (client-side)”Vanilla — dispatch on document.body, let installToast() render:
document.body.dispatchEvent(new CustomEvent('hc:toast', { bubbles: true, detail: { message: 'Saved', variant: 'success' },}));_hyperscript — same dispatch, no helper:
<button class="hc-button" _="on click send hc:toast(message:'Saved', variant:'success') to <body/>"> Save</button>installToast() still has to be running somewhere to actually render
the toast. _hyperscript only replaces the call site, not the
listener.
Close dialog after a successful request
Section titled “Close dialog after a successful request”Vanilla — installCloseDialog() watches htmx:afterRequest for any
ancestor with data-hc-close-dialog-on-success:
<form data-hx-post="/items" data-hx-target="closest dialog" data-hx-swap="outerHTML" data-hc-close-dialog-on-success> ...</form>_hyperscript — listen on the same event, scoped to this element:
<form data-hx-post="/items" data-hx-target="closest dialog" data-hx-swap="outerHTML" _="on htmx:afterRequest if event.detail.successful call closest <dialog/> then call it.close() end"> ...</form>Close popover after a successful request
Section titled “Close popover after a successful request”<form data-hx-get="/items" data-hx-target="#results" _="on htmx:afterRequest if event.detail.successful call closest <[popover]/> then call it.hidePopover() end"> ...</form>Remote dialog (open a server-rendered dialog after swap)
Section titled “Remote dialog (open a server-rendered dialog after swap)”Vanilla — installRemoteDialog() watches htmx:afterSwap on any
[data-hc-remote-dialog-root]:
<div id="dialog-root" data-hc-remote-dialog-root></div>_hyperscript — same idea, inline:
<div id="dialog-root" _="on htmx:afterSwap set dlg to my.querySelector('dialog') if dlg and not dlg.open then dlg.showModal()"></div>Reacting to component events
Section titled “Reacting to component events”The interactive components (menu, combobox, command, calendar, toggle
group, input OTP, splitter, tabs, …) keep their internals in the
vanilla behaviors — that is where the WAI-ARIA keyboard model, focus
management, and roving tabindex live, tested once and consistent
everywhere. What they expose to you is a small set of bubbling
hc:* events. That seam is fully declarative: handle it with
_="on hc:…" in _hyperscript, exactly as you would with htmx’s
data-hx-trigger.
| Component | Event | event.detail |
|---|---|---|
| Menu / Context menu | hc:menuselect | { item, menu, checked?, contextTarget? } |
| Combobox | hc:comboboxselect | { value, label, option, input } |
| Multicombobox | hc:multicomboboxchange | { values, added, removed, input } |
| Command | hc:commandselect | { item, value, command } |
| Calendar | hc:calendarchange | { value, date } |
| Input OTP | hc:otpchange · hc:otpcomplete | { value, input } |
| Splitter | hc:splitterchange | { value, orientation } |
| Toggle group | hc:togglegroupchange | { type, value? , values?, item, pressed? } |
| Tabs | hc:tabactivated | (dispatched on the activated panel) |
These bubble, so you can listen on the component or any ancestor.
Show the chosen calendar date:
<div class="hc-calendar" data-value="2026-05-15" aria-label="Pick a date" _="on hc:calendarchange put event.detail.value into #picked"></div><output id="picked"></output>Auto-submit when the OTP fills up (no JS, no hx-trigger needed):
<form data-hx-post="/verify"> <div class="hc-inputotp" data-length="6" _="on hc:otpcomplete call closest <form/> then call it.requestSubmit()"> <input class="hc-inputotp__input" type="text" name="code" aria-label="Code"> </div></form>Run a command-palette action:
<div class="hc-command" _="on hc:commandselect if event.detail.value is 'home' then go to url '/' else if event.detail.value is 'new' then go to url '/new/'"> ...</div>Persist the splitter position across visits:
<div class="hc-splitter" data-orientation="horizontal" _="on load if localStorage.split then set @data-value to localStorage.split on hc:splitterchange set localStorage.split to event.detail.value"> ...</div>React to a toggle-group / menu choice:
<div class="hc-toggle-group" role="radiogroup" data-type="single" aria-label="View" _="on hc:togglegroupchange add .is-{event.detail.value} to #grid"> ...</div>
<div class="hc-menu" id="row-menu" popover role="menu" _="on hc:menuselect call handleRow(event.detail.item.dataset.action)"> ...</div>Because these are ordinary DOM events, the same handler works in
htmx — swap _="on hc:calendarchange …" for
data-hx-trigger="hc:calendarchange" when the reaction is a server
request rather than a local DOM change. Use _hyperscript for the local
glue, htmx for the network call; the component does not care which is
listening.
When not to use _hyperscript
Section titled “When not to use _hyperscript”- You ship a bundle anyway. If your build already produces a
modules bundle, the vanilla behaviors compose with it for free.
Pulling in
_hyperscriptfor half a kilobyte of orchestration is not a great tradeoff. - The behavior is shared across many elements. A single
installConfirm()covers every[data-hc-confirm]on the page. Inlining the same _hyperscript on every button is repetition. - You want types or unit tests. The vanilla helpers expose JSDoc
and ship with
.d.ts. _hyperscript is a string in an attribute; type-aware tooling will not look inside it.
Related
Section titled “Related”- Plain HTML — minimal setup.
- Confirm action recipe — vanilla and _hyperscript forms side by side.
- _hyperscript Reference — the language docs.